ThreadLocal学习记录
ThreadLocal之前理解的一直很模糊,决定花点时间好好学习一下。最初接触ThreadLocal是在JVM的引用类型谈到弱引用(Weak Reference)的时候,会出现内存泄漏、脏数据等问题。
首先ThreadLocal从字面看是线程本地变量的意思,它不是一个线程,只是为每个线程维护了一个独立的变量副本。这个副本的数据是当前线程独享的,也就是说每个线程只可以访问自己的变量副本,同时进行各种操作时不会影响其他线程的变量副本。所以,ThreadLocal到底在什么场景下使用呢?当每个线程需要某些变量在线程中独立,但是在方法中共享,就可以使用ThreadLocal对象去设置这些变量。
public class Main{
static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
System.out.println(threadLocal.get());
threadLocal.set(1);
System.out.println(threadLocal.get());
});
Thread t2 = new Thread(()->{
System.out.println(threadLocal.get());
threadLocal.set(2);
System.out.println(threadLocal.get());
});
t1.start();
t1.join();
t2.start();
}
}
null
1
null
2
可以发现线程间共享TheadLocal对象,而Threadlocal对象维护的变量在线程间是隔离的。线程1执行过set后,线程2的ThreadLocal第一次get()值为null就可以说明。真要将ThreadLocal的内容写成文章记录,发现思路还是比较乱的,还是决定接着探讨ThreadLocal的原理,然后再从源码解释加深印象。
TheadLocal原理
先给出Thread 、ThreadLocal和ThreadLocalMap的关系图(图片来自《码出高效:Java开发手册》)。
从图中得出:
- 一个线程有且只有一个ThreadLocalMap对象
- 一个Entry对象的Key弱引用指向一个ThreadLocal对象
- 一个ThreadlocalMap对象存储多个Entry对象
- 一个ThreadLocal对象可以被多个线程共享
- ThreadLocal对象不持有Value,Value由线程的Entry对象持有。
使用IDEA查看ThreadLocal源码,快捷键 Ctrl+F12 查看其中的方法和子类等。其中的ThreadLocal中有一个静态内部类ThreadLocalMap,而ThreadLocalMap中也有一个静态内部类Entry。同时ThreadLocal与ThreadLocalMap中都定义了三个重要的方法get()/getEntry()、set()、remove()。我们可以简单看下源码:
//ThreadLocal的get()方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //发现最后的操作其实是ThreadLocalMap在实现
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//ThreadLocal的set()方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//ThreadLocal的remove()方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
查看其他两种方法也可以发现,ThreadLocal的上述三种方法都是由ThreadLocalMap完成实现的。再来看一下ThreadLocalMap的源码:
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;
}
}
.
.
.
.
}
再来一张比较简单清晰的图:
实线表示强引用,虚线表示弱引用。对于强引用的回收,如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
所以,为了避免内存泄漏,我们的想法是这样的,将ThreadLocal对象引用置为null,这样ThreadLocal对象弱引用的key就会在下一次YGC的时候自动被回收.需要注意的是,
这时候还存在着一条强引用链Thread对象引用->Thread->ThreadLocalMap -> entry -> value -> 线程变量
,如果不做任何操作就会使value无法被回收,也无法被使用,导致内存泄漏。
为了防止这种现象出现,ThreadLocal提供有get()、set()、和remove()方法,将key==null的value置为null,这样取消强引用关联,使得jvm在合适的时间将value回收,防止出现内存泄漏。但是,源码告诉我们get()和set()并不能完全防止内存泄漏,remove()才是最保险的
。具体分析可以看这篇博客我惊了!!!ThreadLocal 源码存在内存泄露的 Bug!!!
总结一下:
ThreadLocal问题一:产生脏数据
使用线城池重复调用线程对象的时候,与该线程绑定的ThreadLocal变量也会重复使用,当不及时remove()与线程有关的信息,那么下一次调用该线程若不是用set()设置初始值,就会get()到之前线程被使用过的信息,就产生了脏数据。
ThreadLocal问题二:内存泄漏
上面已经分析过,不及时使用remove()操作,会导致value无法被回收,该块内存也无法使用,导致内存泄漏。