使用 ThreadLocal 维护变量时,ThreadLocal为每个使用该变量的线程提供单独的变量副本,所以每一个线程都可以独立地改变自己的副本,不会影响其它线程所对应的数据。
一个示例使用如下:
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal(); // 创建线程本地变量
threadLocal.set("It is main thread"); // 主线程设置数据
new Thread(() -> {
threadLocal.set("It is thread1"); // 子线程1设置数据
System.out.println(threadLocal.get().toString());
}).start();
new Thread(() -> {
threadLocal.set("It is thread2"); // 子线程2设置数据
System.out.println(threadLocal.get().toString());
}).start();
System.out.println(threadLocal.get());
}
运行结果:
It is thread1
It is main thread
It is thread2
子线程1率先打印了ThradLocal 变量存储的数据, 之后主线程运行,打印其ThradLocal 变量存储的数据,可以看出虽然多个线程对同一个ThradLocal变量进行设置,但是 ThreadLocal 中存储的数据并没有相互影响.
此处以Jdk 1.8 中 ThreadLocal 的get() 方法源码来分析原理.
public T get() {
Thread var1 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);
if (var2 != null) {
ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
if (var3 != null) {
Object var4 = var3.value;
return var4;
}
}
return this.setInitialValue();
}
get()方法完成了以下事情:
调用Thread.currentThread()
获取当前线程对象;
根据当前线程对象,调用getMap(Thread)
获取线程对应的ThreadLocalMap
对象:
ThreadLocal.ThreadLocalMap getMap(Thread var1) {
return var1.threadLocals; // threadLocals是Thread类的成员变量,初始化为null.
}
如果获取的map不为 null,则在map中以当前 ThreadLocal 对象作为key去map中获取对应的键值对Entry.获取的map为 null 则说明当前线程 ThreadLocalMap 还未初始化,需要进入步骤5.
若键值对 Entry 不为null,则返回其中存储的value值,否则进入步骤5.
调用setInitialValue()方法,对线程的 ThreadLocalMap 对象进行初始化操作,ThreadLocalMap对象保存的 Entry 的key为 ThreadLocal 实例对象,value为initialValue()方法的返回值
.
从上面的分析可以看到 ThreadLocal 的实现依赖于ThreadLocalMap
类, 而ThreadLocalMap 是 ThreadLocal的静态内部类.每个Thread维护一个ThreadLocalMap映射表,这个映射表中存储的键值对Entry的key为ThreadLocal对象本身,value则是需要存储的Object. 由此也可知一个ThreadLocal实例只能存储一条数据,不过线程中维护的ThreadLocalMap表可以存储多个Entry, 故一个线程可以通过多个ThreadLocal实例来存储多个本地变量.
ThreadLocal 在 JVM 内存中的引用关系如下图所示, 实线为强引用,虚线为弱引用
其中Entry
的定义如下,可知ThreadLocalMap
是使用 ThreadLocal 的弱引用作为Key
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> var1, Object var2) {
super(var1);
this.value = var2;
}
}
如我们所知,弱引用对象在Java虚拟机进行垃圾回收时就会被释放, 那就有必要考虑以下问题:
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用关联,那么在虚拟机进行垃圾回收时,这个ThreadLocal会被立即回收.此时ThreadLocalMap中就会出现key为null的Entry,这些key对应的value也就无法访问了. 但是value却存在一条从Current Thread过来的强引用链,因此只有当Current Thread销毁时,value占用的内存才能得到回收.
强引用链如下:
CurrentThread Ref -> Thread -> ThreadLocalMap -> Entry -> value
通常情况下新建线程完成任务后肯定是要被销毁的,这样也就没什么问题.但在线程对象不被回收的情况下,例如使用线程池时,核心线程是一直在运行的,线程对象不会被回收, 那就可能出现内存泄露的问题.
ThreadLocalMap 的设计中已经考虑到这种情况, 其对应的方案是在 ThreadLocal 的get(),set(),remove()方法调用的时候都会清除线程 ThreadLocalMap 里所有key为null的value
.
private int expungeStaleEntry(int var1) {
ThreadLocal.ThreadLocalMap.Entry[] var2 = this.table;
int var3 = var2.length;
var2[var1].value = null; // var1为 Entry 数组中 key 为 null 的Entry的下标
var2[var1] = null;
--this.size;
......
}
但这些被动的预防措施并不能保证不会内存泄漏:
ThreadLocal
的线程在线程池中不销毁也不再被使用,那么 value 占用的这块内存一直存在,会导致内存泄漏.ThreadLocal
的线程在之后的任务中不再使用线程本地变量,也就是不调用get(),set(),remove()方法,那么value 占用的这块内存依然不会被释放回收,存在内存泄漏.