目录
ThreadLcoal 源码浅析
ThreadLocal 的垃圾回收
Java 引用
ThreadLocal 的回收
各线程中 threadLocalMap 的回收
内存泄露问题
总结
参考
ThreadLcoal 源码浅析
我们知道 ThreadLocal 用于维护多个线程线程独立的变量副本, 这些变量只在线程内共享, 可跨方法, 类等, 如下是一个维护多个线程 Integer 变量的 ThreadLocal:
ThreadLocal<Integer> threadLocalNum = new ThreadLocal<>();
每个使用 threadLocalNum 的线程, 可以通过形如 threadLocalNum.set(1)的方式创建了一个独立使用的 Integer 变量副本, 那么它是怎么实现的呢? 我们今天就来简单的分析一下.
先看下 ThreadLocal 的 set 方法是如何实现的, 源码如下:
- public void set(T value) {
- Thread t = Thread.currentThread(); // 获取当前线程
- ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap
- if (map != null)
- map.set(this, value); // 当前线程的 ThreadLocalMap 不为空则直接设值
- else
- createMap(t, value); // 当前线程的 ThreadLocalMap 为空则创建一个来设置值
- }
是的, 你没有看错, 是获取当前线程中的 ThreadLocalMap 来设置的值, 我们来看一下 getMap(t)是如何实现的:
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
然后我们看到 Thread 中包含了一个 ThreadLocalMap 类型的属性:
ThreadLocal.ThreadLocalMap threadLocals = null;
到这里我们可以得出一个结论: 各个线程持有了一个 ThreadLocalMap 的属性, 通过 ThreadLocal 设置变量时, 直接设置到了对应线程的的 ThreadLocalMap 属性中.
那么不同的线程中通过 ThreadLocal 设置的值是如何关联定义的 ThreadLocal 变量和 Thread 中的 ThreadLocalMap 的呢? 我们接着分析.
前面写到当前线程的 ThreadLocalMap 为空则创建一个 ThreadLocalMap 来设值, 我们来看下 createMap(t, value)的具体实现:
- void createMap(Thread t, T firstValue) {
- t.threadLocals = new ThreadLocalMap(this, firstValue);
- }
- ///////////////////
- //ThreadLocalMap 构造器定义如下
- ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
- table = new Entry[INITIAL_CAPACITY];
- int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //
- table[i] = new Entry(firstKey, firstValue);
- size = 1;
- setThreshold(INITIAL_CAPACITY);
- }
- private static final int INITIAL_CAPACITY = 16;
线程中 threadLocals 是一个 ThreadLocalMap 变量, 其默认值是 null, 该线程在首次使用 threadLocal 对象调用 set 的时候通过 createMap(Thread t, T firstValue)实例化.
先来看一下 ThreadLocalMap, 它是在 ThreadLocal 中定义的一个静态内部类, 其内属性如下:
- /**
- * The initial capacity -- MUST be a power of two.
- */
- private static final int INITIAL_CAPACITY = 16;
- /**
- * The table, resized as necessary.
- * table.length MUST always be a power of two.
- */
- private Entry[] table;
- /**
- * The number of entries in the table.
- */
- private int size = 0;
- /**
- * The next size value at which to resize.
- */
- private int threshold; // Default to 0
其中属性 private Entry[] table, 用于存储通过 threadLocal set 进来的变量, Entry 定义如下:
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
Entry 继承了 WeakReference<ThreadLocal<?>>,ThreadLocal 在构造器中被指定为弱引用 super(k)(后面会单独讨论为何这里使用弱引用).
至此, 我们可以知道 ThreadLocal 和 Thead 的内存结构如下:
ThreadLocal 的垃圾回收
网上看到很多文章都在讲 ThreadLocal 的内存泄露问题, 所以也在这里简单说一下自己的理解.
从上面的结构可以看出 ThreadLocal 涉及到的要回收的对象包括:
ThreadLocal 实例本身
各线程中的 threadLocalMap, 其中包括各个 Entry 的 key, value
下面先简述 java 的引用, 然后分别讨论 ThreadLocal 本身的回收和 threadLcoalMap 的回收
Java 引用
强引用(StrongReference): 对象可达就不会被 gc 回收, 空间不足时报 error
软引用(SoftReference): 对象无其他强引用, 当空间不足时才会被 gc 回收.
弱引用(WeakReference): 对象无其他强引用, gc 过程扫描到就会被回收.
ThreadLocal 的回收
ThreadLocal 实例的引用主要包括两种:
ThreadLocal 定义处的强引用
各线程中 ThreadLocalMap 里的 key=weak(threadLocal), 是弱引用
强引用还在的情况下 ThreadLocal 一定不会被回收; 无强引用后, 由于各个 Thread 中 Entry 的 key 是弱引用, 会在下次 GC 后变为 null.ThreadLocal 实例什么时候被回收完全取决于强引用何时被干掉, 那么什么时候强引用会被销毁呢? 最简单的就是 threadLocal=null 强引用被赋值为 null; 其它也可是 threadLocal 是一个局部变量, 在方法退出后引用被销毁, 等等.
这里来回答一下前面提到的为什么 ThreadLocalMap 中将 key 设计为弱引用, 我们假设如果 ThreadLocalMap 中是强引用会出现什么情况? 定义 ThreadLocal 时定义的强引用被置为 null 的时候, 如果还有其它使用了该 ThreadLocal 的线程没有完成, 还需要很久会执行完成, 那么这个线程将一直持有该 ThreadLocal 实例的引用, 直到线程完成, 期间 ThreadLocal 实例都不能被回收, 最重要的是如果不了解 ThreadLocal 内部实现, 你可能都不知道还有其他线程引用了 threadLocal 实例.
线程结束时清除 ThreadLocalMap 的代码 Thread.exit()如下:
- /**
- * This method is called by the system to give a Thread
- * a chance to clean up before it actually exits.
- */
- private void exit() {
- if (group != null) {
- group.threadTerminated(this);
- group = null;
- }
- /* Aggressively null out all reference fields: see bug 4006245 */
- target = null;
- /* Speed the release of some of these resources */
- threadLocals = null;
- inheritableThreadLocals = null;
- inheritedAccessControlContext = null;
- blocker = null;
- uncaughtExceptionHandler = null;
- }
各线程中 threadLocalMap 的回收
单从引用的角度来看, 各线程中的 threadLocalMap, 其中包括各个 Entry 的 key 和 value, 线程 (也就是 Thread 实例) 本身一直持有 threadLocalMap 的强引用, 只有在线程结束的时候才会被回收, 但是 ThreadLocal 在实现的时候提供了一些方法: set/get/remove, 可以在执行它们的时候回收其它已经失效 (key=null) 的 entry 实例.
这里就以 set 为例看看 ThreadLocal 是如何回收 entry 的, ThreadLocal set 方法实现如下:
- //ThreadLocal
- public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value); // 本次要分析的方法
- else
- createMap(t, value); // 这里前面已经分析了
- }
- //ThreadLocalMap
- private void set(ThreadLocal<?> key, Object value) {
- // We don't use a fast path as with get() because it is at
- // least as common to use set() to create new entries as
- // it is to replace existing ones, in which case, a fast
- // path would fail more often than not.
- Entry[] tab = table;
- int len = tab.length;
- int i = key.threadLocalHashCode & (len-1); // 获取当前 threadLocal 实例的 hashcode, 同时也是 table 的下标
- // 这里 for 循环找 key, 是因为 hash 冲突会使 hashcode 指向的下标不是真实的存储位置
- for (Entry e = tab[i];
- e != null;
- e = tab[i = nextIndex(i, len)]) {
- ThreadLocal<?> k = e.get();
- // 找到了设置为新值
- if (k == key) {
- e.value = value;
- return;
- }
- //entry 不为 null,key 为 null
- // 说明原来被赋值过, 但是原 threadLocal 已经被回收
- if (k == null) {
- replaceStaleEntry(key, value, i);
- return;
- }
- }
- // 如果下标对应的 entry 为 null, 则新建一个 entry
- tab[i] = new Entry(key, value);
- int sz = ++size;
- // 清理 threadlocal 中其它被回收了的 entry(也就是 key=null 的 entry)
- if (!cleanSomeSlots(i, sz) && sz>= threshold)
- //rehash
- rehash();
- }
看一下 cleanSomeSlots 的实现:
- //ThreadLocalMap
- private boolean cleanSomeSlots(int i, int n) {
- boolean removed = false;
- Entry[] tab = table;
- int len = tab.length;
- do {
- // 获取下一个 entry 的下标
- i = nextIndex(i, len);
- Entry e = tab[i];
- //entry 不为 null,key 为 null
- // 说明原来被赋值过, 但是原 threadLocal 已经被回收
- if (e != null && e.get() == null) {
- n = len;
- removed = true;
- // 删除已经无效的 entry
- i = expungeStaleEntry(i);
- }
- } while ( (n>>>= 1) != 0);
- return removed;
- }
- private int expungeStaleEntry(int staleSlot) {
- Entry[] tab = table;
- int len = tab.length;
- // 回收无效 entry
- tab[staleSlot].value = null;
- tab[staleSlot] = null;
- size--;
- // Rehash until we encounter null
- Entry e;
- int i;
- for (i = nextIndex(staleSlot, len);
- (e = tab[i]) != null;
- i = nextIndex(i, len)) {
- ThreadLocal<?> k = e.get();
- //entry 不为 null,key 为 null, 应该回收
- if (k == null) {
- e.value = null;
- tab[i] = null;
- size--;
- } else {
- //rehash 的实现
- // 计算当前 entry 的 k 的 hashcode, 看是下标是否应该为 i
- // 如果不为 i 说明, 是之前 hash 冲突放到这儿的, 现在需要 reash
- int h = k.threadLocalHashCode & (len - 1);
- //h!=i 说明 hash 冲突了, entry 不应该放在下标为 i 的位置
- if (h != i) {
- tab[i] = null;
- // Unlike Knuth 6.4 Algorithm R, we must scan until
- // null because multiple entries could have been stale.
- // 找正确的位置 h, 但是还是有可能冲突所以要循环
- while (tab[h] != null)
- h = nextIndex(h, len);
- tab[h] = e;
- }
- }
- }
- return i;
- }
从上面的分析我们可以看到把 ThreadLocalMap 中的 key 设计为 weakReference, 也使 set 方法可以通过 key==null && entry != null 判断 entry 是否失效.
总结一下 ThreadLocal set 方法的实现:
根据 threadLocal 计算 hashcode 找到 entry[]数组对应位置设置值
遍历数组找到其它失效的 (entry 不为 null,key 为 null) 的 entry 删除
内存泄露问题
ThreadLocal 通过巧妙的设计最大程度上减少了内存泄露的可能, 但是并没有完全消除.
当我们使用完 ThreadLocal 后没有调用 set/get/remove 方法, 那么可能会导致失效内存不能及时被回收, 导致内存泄露, 尤其是在 value 占用内存较大的情况.
所以最佳实践是, 在明确 ThreadLocal 不再使用时, 手动调用 remove 方法及时清空.
总结
ThreadLocal 并不解决线程间共享数据的问题
ThreadLocal 是通过让线程内的 ThreadLocalMap.Entry 的 key 指向自身, 来实现了对线程内对象的引用, 从而可以在线程内方便的使用变量. 同时因为操作的都是线程内的变量, 也避免了实例线程安全的问题
ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用, 避免了 ThreadLocal 对象无法被回收的问题
ThreadLocalMap 的 set 方法通过调用 cleanSomeSlots 方法回收键为 null 的 Entry 对象的值 (即失效实例) 从而防止内存泄漏(其它的 remove,get 类似)
在明确 ThreadLocal 不再使用时, 手动调用 remove 方法及时清空
参考
正确理解 Thread Local 的原理与适用场景 http://www.jasongj.com/java/threadlocal/
来源: https://www.cnblogs.com/chrischennx/p/9557285.html