ThreadLocal理解及其内存泄露
-
ThreadLocal可以理解为,为一个线程隔离的变量,他不是一个集合,他只是一个类,这个类可以set,get ,remove
-
set时使用的是哪个线程,get时,也必需用哪个线程去获取,才能获取到值
-
set的值,同时只能存在一个,后面set的值,会把前面的给覆盖了
示例代码1 证明一个 ThreadLocal只能保存一个对象
public static void main(String[] args) throws IOException {
ThreadLocal<Person> threadLocal = new ThreadLocal<>();
threadLocal.set(new Person("ysh"));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Person person = threadLocal.get();
System.out.println("person = " + person);
System.out.println("______________________");
threadLocal.set(new Person("hua"));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Person person1 = threadLocal.get();
System.out.println("person1 = " + person1);
}
输出:
person = Person{name='ysh'}
______________________
person1 = Person{name='hua'}
可以看到,一个ThreadLocal只能设置一个,当一个新的值设置进来的时候,他就会被覆盖了。
示例代码2 证明ThreadLocal是线程私有的
public static void main(String[] args) throws IOException {
ThreadLocal<Person> threadLocal = new ThreadLocal<>();
new Thread("t1") {
@Override
public void run() {
threadLocal.set(new Person("ysh"));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Person person = threadLocal.get();
System.out.println("t1 get = " + person);
}
}.start();
new Thread("t2") {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Person person1 = threadLocal.get();
System.out.println("t2 get = " + person1);
}
}.start();
System.in.read();
}
输出结果:
t1 get = Person{name='ysh'}
t2 get = null
上面代码的意思是,在t1线程中,设置了一个person对象,在t2线程中去获取,因为,t2线程获取的时候,是等了2秒,所以,肯定t2在t1后面获取,但从输出结果来看,在t2线程中获取不到t1设置的值。
源码分析
调用 threadLocal.set(new Person(“ysh”))时,会调用下面的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);
}
}
可以看到,getMap的入参是一个线程,而且是当前线程。下面是getMap的源码:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可见,getMap当前这个t线程的threadLocals属性给返回了。什么是threadLocals呢?
threadLocals
打开Thread的源码,可以看到
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
Thread类中有一个属性叫 threadLocals 的,说明,每个Thread线程都有自己一个单独的Map,这个类型是 ThreadLocal.ThreadLocalMap ,如下:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
这个threadLocals的意思是,每个线程可以存入自己这个线程私有的很多个ThreadLocal,键是ThreadLocal类型,值是传递进来的任意对象
那么,这个ThreadLocal.ThreadLocalMap又是什么呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
从上图可以看出,这个 ThreadLocalMap
其实是一个 ThreadLocal的一个静态内部类,其中,重要结构是在上图也能看到,是一个Entry的类型,那么,什么又是 Entry我们一会再看。
现在先看看,当我们调用 ThreadLocalMap
时,都干了什么
map.set(this, value);
set方法中,获取每个线程中的ThreadLocalMap,然后,将 threadLocal 对象作为键,将Person对象作为值,传递给了 map.set(this, value); 方法,在这个方法中,重要的是调用了下面这行代码
tab[i] = new Entry(key, value);
其中,key和value的值见下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
key就是自己new 的ThreadLocal对象,键是Person对象
保存在每个线程单独的map中。
可以看到,这里new 了一个Entry,那么到底什么是Entry呢???
Entry
要说Entry,让我们来先想一下HashMap,他是我们最常用的一个Map类型,他内部的结构是一个个的Node,每当传入一个key和value时,都要进行hashcode,去计算每个value放在index的位置,随着数量的增加,会自动扩容,后面还会变成红黑树的结构,这里就不再展开了(其实也忘的差不多了)
总之,这个Entry,就是类比HashMap中的每一个Node,每个Entry中保存着一个k和一个v
然后,保存这个Entry结构的类,就是上面说的 ThreadLocalMap,从后缀可以看到,他就是一种map。只是内部的每个结点使用的是全新的Entry结构。
仔细看,这个Entry 是 ThreadLocalMap 的一个静态内部类,他继承 WeakReference。是一个弱引用。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
既然他是一个弱引用,那弱引用是一种 只要JVM gc,就一定会被回收的的一种引用,到这里,先不说为什么要使用弱引用,先来总结一下。
小总结
每一个Thread线程中有自己一个单独的map,这个map是ThreadLocalMap,键是ThreadLocal,值是任意类型。然后,这个ThreadLocalMap其内部的每个结点的数据结构是一个个的Entry,Entry是一个弱引用。
问1:为什么ThreadLocal可以做到线程隔离??
因为,使用ThreadLocal对象调用其set方法的时候,他将值保存到每个线程单独的ThreadLocalMap当中,所以,是线程隔离。
问2:为什么在同一个线程中重新调用ThreadLocal对象的set方法重置新的值,为覆盖旧的值呢?
这个很好理解吧,类比HashMap,当设置同一个key时,后面的会把前面的值给覆盖掉,这里,因为键是 ThreadLocal对象,所以,在同一个线程中,使用同一个ThreadLocal对象的set方法是时,就是设置到了同一个map中,就会导致覆盖。
接下来,让我们来想想为什么Entry内部要使用弱引用?
为什么要弱引用
先上图:我从知乎上偷的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
弱引用是一种 只要JVM gc,就一定会被回收的的一种引用
假设使用强引用
假设Entry中的key使用的是强引用,那么 ,就存在这种情况,当threadLocal对象使用下面代码创建后,
ThreadLocal<Person> threadLocal = new ThreadLocal<>();
当threadLocal变量被回收时,threadLocal=null,左侧的实线就断了,但因为真正的保存 ThreadLocal在堆中,因为其还有强引用指向Entry中的key,导致还无法被回收。导致内存泄露。
那其内部使用了 弱引用就一定不会内存泄露吗?
答案是 : 不一定
如果一个ThreadLocal不存在外部强引用时,ThreadLocal势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。
但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
那么,说 了这么多,ThreadLocal的正确使用方法是什么呢?
ThreadLocal正确的使用方法
- 每次使用完ThreadLocal都调用它的remove()方法清除数据
- 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
private static ThreadLocal<Person> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws IOException {
new Thread("t1") {
@Override
public void run() {
threadLocal.set(new Person("ysh"));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Person person = threadLocal.get();
System.out.println("t1 get = " + person);
// 使用完毕,一定要调用remove方法
threadLocal.remove();
}
}.start();
}