讲透ThreadLocal
ThreadLocal
很容易让人望文生义,想当然地认为是一个本地线程,实际上这是一种错误的理解。实际上ThreadLocal
并不是一个 Thread
,而是 Thread
的一个局部变量,这点从源码中就能看出来。
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
当使用 ThreadLocal
维护变量时,ThreadLocal
为每个使用该变量的线程 提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal
既然是线程局部变量,那么理所当然就应该存储在自己的线程对象中,从源码中可以看到线程局部变量是存储在 Thread
对象的 threadLocals
属性中,而 threadLocals
属性是一个 ThreadLocal.ThreadLocalMap
对象。
一、Thread 、ThreadLocal 和 ThreadLocalMap 之间的关系?
Thread
中的threadLocals
属性就是ThreadLocal.ThreadLocalMap
ThreadLocalMap
为ThreadLocal
的静态内部类
public class ThreadLocal<T> {
// 省略部分代码 ...
static class ThreadLocalMap {
// 静态内部类
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// ThreadLocalMap 的初始容量
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
}
// 省略部分代码 ...
}
通过这段代码可以画出如下关系图:
总结:一个 Thread
中只有一个 ThreadLocalMap
,一个 ThreadLocalMap
中可以有多个ThreadLocal
对象,其中一个 ThreadLocal
对象对应一个 ThreadLocalMap
中的一个 Entry
1. ThreadLocalMap 中 Entry 为什么要继承 WeakReference ?
在 Java 里面存在着强引用、弱引用、软引用和虚引用
A a = new A();
B b = new B();
// 考虑这样的情况
C c = new C(b);
b = null;
对于这段代码,考虑 GC 的情况。要知道 b 被置为 null,那么是否意味着一段时间后 GC 可以回收 b 所分配的内存空间呢?
答案是否定的,因为即便 b 被置为 null,但是 c 仍然持有对 b 的引用,而且还是强引用,所以 GC 不会回收 b 原先所分配的空间。导致既不能回收利用,又不能使用,这就造成了内存泄露。
这种情况下该如何防止内存泄漏呢?
可以让 c = null 或者 使用弱引用,如 WeakReference w = new WeakReference(b)
所以根据 Thread 、ThreadLocal 和 ThreadLocalMap
之间的关系,可以分析出他们在堆栈内存中的引用关系如下图:
所以 Entry
继承 WeakReference
是为了防止内存泄漏的处理方式而已
- ThreadLocal 使用到了弱引用,是否意味着不会存在内存泄露呢?
如果把 ThreadLocal
置为 null,那么意味着堆内存中的 ThreadLocal
实例不再有强引用指向,只有弱引用存在,因此 GC 是可以回收这部分空间的,也就是key是可以回收的。但是 value 却存在一条从Thread
对象过来的强引用。因此只有当 Thread
销毁时,value才能得到释放。
所以只要这个 Thread
对象被 GC 回收,就不会出现内存泄露,但在 ThreadLocal
设为 null 和线程结束这段时间内不会被回收的,就发生了我们认为的内存泄露。更重要的是在使用线程池的时候,线程结束是不会销毁的,再次使用的,就可能出现内存泄露。
- 那么
ThreadLocal
中是如何避免这种情况发生的呢?
在 ThreadLocalMap
中的 set/getEntry
方法中,会对 key
为 null(即是 ThreadLocal
为 null)进行判断,如果为null的话,那么是会对 value 置为 null。同时可以通过调用 ThreadLocal
的remove 方法进行释放
注意: ThreadLocal
在没有使用线程池时,正常情况下不会存在内存泄露。但是如果使用了线程池的话,就依赖于线程池的实现,如果线程池不销毁线程的话,那么就会存在内存泄露。所以我们在使用线程池的时候,使用 ThreadLocal
要格外小心!
二、ThreadLocal
是如何做到为每一个线程维护变量副本的呢?
通过上面的分析我们知道了在 ThreadLocal
类中定义了 ThreadLocalMap
,用于存储每一个线程的局部变量,ThreadLocalMap
的键值是 ThreadLocal
,值为线程局部变量
1.ThreadLocal
的 set 方法
由于每个 Thread
实例都有一个 ThreadLocalMap
,所以在进行 set 的时候,首先根据Thread.currentThread()
获取当前线程,然后根据当前线程t,调用 getMap(t)
获取 ThreadLocalMap
对象。如果是第一次设置值,ThreadLocalMap
对象是空值,所以会进行初始化操作,即调用createMap(t,value)
方法。
可以看到 set(T value)
方法为每个 Thread
对象都创建了一个 ThreadLocalMap
,并且将 value
放入 ThreadLocalMap
中。
2.ThreadLocal
的 get 方法
首先获取 ThreadLocalMap
对象,由于 ThreadLocalMap
使用的当前的 ThreadLocal
作为key,所以传入的参数为 this,然后调用 getEntry
方法,通过这个key构造索引,根据索引去 table(Entry数组)中去查找线程本地变量,根据下标找到 Entry 对象,然后判断 Entry 对象e不为空 并且 e的引用与传入的key一样则直接返回,如果找不到则调用 getEntryAfterMiss
方法。调用 getEntryAfterMiss
表示直接散列到的位置没找到,那么顺着hash表循环地往下找,从i开始,一直往下找,直到出现空的槽为止。
三、ThreadLocal
如何回收内存?
1. 在 ThreadLocal 层面的内存回收
当线程死亡时,那么所有的保存在的线程局部变量就会被回收,其实这里是指线程 Thread
对象中的 ThreadLocal.ThreadLocalMap threadLocals
会被回收
2. ThreadLocalMap 层面的内存回收
如果线程可以活很长的时间,那么就涉及到在线程的生命期内如何回收 ThreadLocalMap
的内存了,不然的话ThreadLocalMap
保存的 Entry 对象越多,那么 ThreadLocalMap
就会越来越大,占用的内存就会越来越多,所以对于已经不需要了的线程局部变量,就应该清理掉其对应的 Entry 对象。
为了解决这个问题,Entry 对象的 key 是 WeakReference
的包装,当 ThreadLocalMap
的 private Entry[] table
,已经被占用达到了三分之二时 threshold = 2/3
(也就是线程拥有的局部变量超过了10个) ,就会尝试回收 Entry
对象,我们可以看到 ThreadLocalMap.set()
方法中有下面的代码
转载自:https://juejin.cn/post/7268139204298833939