中高级阶段开发者出去面试, 应该躲不开 ThreadLocal 相关问题, 本文就常见问题做出一些解答, 欢迎留言探讨.
ThreadLocal 为 java 并发提供了一个新的思路, 它用来存储 Thread 的局部变量, 从而达到各个 Thread 之间的隔离运行. 它被广泛应用于框架之间的用户资源隔离, 事务隔离等.
但是用不好会导致内存泄漏, 本文重点用于对它的使用过程的疑难解答, 相信仔细阅读完后的朋友可以随心所欲的安全使用它.
内存泄漏原因探索
ThreadLocal 操作不当会引发内存泄露, 最主要的原因在于它的内部类 ThreadLocalMap 中的 Entry 的设计.
Entry 继承了 WeakReference<ThreadLocal<?>>, 即 Entry 的 key 是弱引用, 所以 key'会在垃圾回收的时候被回收掉, 而 key 对应的 value 则不会被回收, 这样会导致一种现象: key 为 null,value 有值.
key 为空的话 value 是无效数据, 久而久之, value 累加就会导致内存泄漏.
- static class ThreadLocalMap {
- static class Entry extends WeakReference<ThreadLocal<?>> {
- Object value;
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
- ...
- }
怎么解决这个内存泄漏问题
每次使用完 ThreadLocal 都调用它的 remove() 方法清除数据. 因为它的 remove 方法会主动将当前的 key 和 value(Entry) 进行清除.
- private void remove(ThreadLocal<?> key) {
- Entry[] tab = table;
- int len = tab.length;
- int i = key.threadLocalHashCode & (len-1);
- for (Entry e = tab[i];
- e != null;
- e = tab[i = nextIndex(i, len)]) {
- if (e.get() == key) {
- e.clear(); // 清除 key
- expungeStaleEntry(i); // 清除 value
- return;
- }
- }
- }
e.clear() 用于清除 Entry 的 key, 它调用的是 WeakReference 中的方法: this.referent = null
expungeStaleEntry(i) 用于清除 Entry 对应的 value, 这个后面会详细讲.
JDK 开发者是如何避免内存泄漏的
ThreadLocal 的设计者也意识到了这一点 (内存泄漏), 他们在一些方法中埋了对 key=null 的 value 擦除操作.
这里拿 ThreadLocal 提供的 get() 方法举例, 它调用了 ThreadLocalMap#getEntry() 方法, 对 key 进行了校验和对 null key 进行擦除.
- private Entry getEntry(ThreadLocal<?> key) {
- // 拿到索引位置
- int i = key.threadLocalHashCode & (table.length - 1);
- Entry e = table[i];
- if (e != null && e.get() == key)
- return e;
- else
- return getEntryAfterMiss(key, i, e);
- }
如果 key 为 null, 则会调用 getEntryAfterMiss() 方法, 在这个方法中, 如果 k == null , 则调用 expungeStaleEntry(i); 方法.
- private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
- Entry[] tab = table;
- int len = tab.length;
- while (e != null) {
- ThreadLocal<?> k = e.get();
- if (k == key)
- return e;
- if (k == null)
- expungeStaleEntry(i);
- else
- i = nextIndex(i, len);
- e = tab[i];
- }
- return null;
- }
expungeStaleEntry(i) 方法完成了对 key=null 的 key 所对应的 value 进行赋空, 释放了空间避免内存泄漏.
同时它遍历下一个 key 为空的 entry, 并将 value 赋值为 null, 等待下次 GC 释放掉其空间.
- private int expungeStaleEntry(int staleSlot) {
- Entry[] tab = table;
- int len = tab.length;
- // expunge entry at staleSlot
- tab[staleSlot].value = null;
- tab[staleSlot] = null;
- size--;
- // Rehash until we encounter null
- Entry e;
- int i;
- // 遍历下一个 key 为空的 entry, 并将 value 指向 null
- for (i = nextIndex(staleSlot, len);
- (e = tab[i]) != null;
- i = nextIndex(i, len)) {
- ThreadLocal<?> k = e.get();
- if (k == null) {
- e.value = null;
- tab[i] = null;
- size--;
- } else {
- int h = k.threadLocalHashCode & (len - 1);
- if (h != i) {
- tab[i] = null;
- // Unlike Knuth 6.4 Algorithm R, we must scan until
- // null because multiple entries could have been stale.
- while (tab[h] != null)
- h = nextIndex(h, len);
- tab[h] = e;
- }
- }
- }
- return i;
- }
同理, set() 方法最终也是调用该方法 (expungeStaleEntry), 调用路径: set(T value)->map.set(this, value)->rehash()->expungeStaleEntries()
remove 方法 remove()->ThreadLocalMap.remove(this)->expungeStaleEntry(i)
这样做, 也只能说尽可能避免内存泄漏, 但并不会完全解决内存泄漏这个问题. 比如极端情况下我们只创建 ThreadLocal 但不调用 set,get,remove 方法等. 所以最能解决问题的办法就是用完 ThreadLocal 后手动调用 remove().
手动释放 ThreadLocal 遗留存储? 你怎么去设计 / 实现?
这里主要是强化一下手动 remove 的思想和必要性, 设计思想与连接池类似.
包装其父类 remove 方法为静态方法, 如果是 spring 项目, 可以借助于 bean 的声明周期, 在拦截器的 afterCompletion 阶段进行调用.
弱引用导致内存泄漏, 那为什么 key 不设置为强引用
这个问题就比较有深度了, 是你谈薪的小小资本.
如果 key 设置为强引用, 当 threadLocal 实例释放后, threadLocal=null, 但是 threadLocal 会有强引用指向 threadLocalMap,threadLocalMap.Entry 又强引用 threadLocal, 这样会导致 threadLocal 不能正常被 GC 回收.
弱引用虽然会引起内存泄漏, 但是也有 set,get,remove 方法操作对 null key 进行擦除的补救措施, 方案上略胜一筹.
线程执行结束后会不会自动清空 Entry 的 value
一并考察了你的 gc 基础.
事实上, 当 currentThread 执行结束后, threadLocalMap 变得不可达从而被回收, Entry 等也就都被回收了, 但这个环境就要求不对 Thread 进行复用, 但是我们项目中经常会复用线程来提高性能, 所以 currentThread 一般不会处于终止状态.
Thread 和 ThreadLocal 有什么联系呢
ThreadLocal 的概念.
Thread 和 ThreadLocal 是绑定的, ThreadLocal 依赖于 Thread 去执行, Thread 将需要隔离的数据存放到 ThreadLocal(准确的讲是 ThreadLocalMap) 中, 来实现多线程处理.
相关问题扩展
加分项来了.
spring 如何处理 bean 多线程下的并发问题
ThreadLocal 天生为解决相同变量的访问冲突问题, 所以这个对于 spring 的默认单例 bean 的多线程访问是一个完美的解决方案. spring 也确实是用了 ThreadLocal 来处理多线程下相同变量并发的线程安全问题.
spring 如何保证数据库事务在同一个连接下执行的
要想实现 jdbc 事务, 就必须是在同一个连接对象中操作, 多个连接下事务就会不可控, 需要借助分布式事务完成. 那 spring 如何保证数据库事务在同一个连接下执行的呢?
DataSourceTransactionManager 是 spring 的数据源事务管理器, 它会在你调用 getConnection() 的时候从数据库连接池中获取一个 connection, 然后将其与 ThreadLocal 绑定, 事务完成后解除绑定. 这样就保证了事务在同一连接下完成.
概要源码:
1. 事务开始阶段: org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin->TransactionSynchronizationManager#bindResource->org.springframework.transaction.support.TransactionSynchronizationManager#bindResource
2. 事务结束阶段:
org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion->TransactionSynchronizationManager#unbindResource->org.springframework.transaction.support.TransactionSynchronizationManager#unbindResource->TransactionSynchronizationManager#doUnbindResource
Java 知音 10 月基础篇
聊聊 HashMap 和 TreeMap 的内部结构
感受 lambda 之美, 推荐收藏, 需要时查阅
多线程基础体系知识清单不
了解 Java 反射机制? 看这篇就行!
从实践角度重新理解 BIO 和 NIO
java Integer 包装类装箱的一个细节
来源: https://www.cnblogs.com/javazhiyin/p/11834121.html