ThreadLocal和Thread的关系
首先明白他们之间的关系,之后才能理解ThreadLocal以及它存在的问题。
ThreadLocal
类有了一个静态类ThreadLocalMap
,Thread
类有一个ThreadLocalMap
类的引用。
public class ThreadLocal<T> {
...
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
...
}
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
我们知道ThreadLocal通过线程隔离的方式来保证共享资源的线程安全。其实就是每个线程中都存储一份共享资源的副本,所以你调用get()
或者set()
的时候其实是调用ThreadLocalMap的get()
和set()
方法,而这个Map是属于当前的线程的。
get()方法
具体来说,当一个某个线程调用ThreadLocal实例的get()方法时候,简化后的过程是:
- 获取当前的Thread
- 拿到当前Thread的ThreadLocalMap
- 遍历Entry拿到key值为ThreadLocal实例的Entry
- 把value返回
set()方法
和get方法差不多的流程:
- 获取当前Thread
- 拿到Thread的ThreadLocalMap
- 调用map的set方法
为什么ThreadLocalMap是弱引用
首先要了解java中的四种引用类型:
- 强引用:只要强引用存在,对象不会被回收
- 软引用:JVM内存不够的时候才会被回收
- 弱引用:只能存活到下次GC之前
- 虚引用:不影响GC回收,只是在对象被回收的时候收到一个通知
假设有如下代码:
ThreadLocal<String> threadlocal = new ThreadLocal<>();
threadlocal.set("xxx");
假设key是强引用,该线程在栈帧创建threadlocal引用,并且是强引用。当线程执行结束,引用关系就没了,但是如果key也是强引用,那么Entry中key还有指向threadlocal实例的引用,导致该对象无法被回收,造成内存泄漏。
所以,key要设计成弱引用,就是为了避免内存泄漏问题的。
内存泄漏:不被使用的对象或者变量的内存没有被回收
有弱引用为什么还会出现内存泄漏
正常情况下是没有什么问题的,但是一般项目中都要使用线程池,用了线程池之后,线程执行结束之后可能不会被销毁,那么即使key是弱引用value也不会被回收,同样会造成内存泄漏。
引用链如下:Thread->ThreadLocalMap->Entry->value
所以,要手动调用remove()方法防止内存泄漏。
总结
- ThreadLocal是通过线程副本隔离的方式来保证线程安全的
- ThreadLocalMap的key是弱引用主要是为了避免内存泄漏
- 在使用线程池的应用场景下有弱引用也会出现内存泄漏,主要原因是线程可能不会被销毁,从而导致引用一直存在,需要手动去remove防止内存泄漏