在早期的JDK版本中,ThreadLocal的内部结构是一个Map,其中每一个线程实例作为Key,线程在“线程本地变量”中绑定的值为Value(本地值)。早期版本中的Map结构,其拥有者为ThreadLocal实例,每一个ThreadLocal实例拥有一个Map实例。
在JDK 8版本中,ThreadLocal的内部结构发生了演进,虽然还是使用了Map结构,但是Map结构的拥有者已经发生了变化,其拥有者为Thread(线程)实例,每一个Thread实例拥有一个Map实例。另外,Map结构的Key值也发生了变化:新的Key为ThreadLocal实例。这样做有如下的好处:
- 当前的java应用在多核大内存的机器上,成千上万的线程很常见,而且大部分都是分布式应用,ThreadLocal变量使用的并不多,一般也就用于计算调用链耗时、登录鉴权等场景,旧的实现是一个map中有大量的元素(因为用Thread做key),新的实现是每个Thread有一个Map,元素很少。ThreadLocalMap是一个Map接口的简单实现,元素越少越容易处理,比如hash冲突、扩容等等
- 线程终止后,线程相关的都会被回收,新的实现可以节约内存
然后有一个问题,ThreadLocalMap中放的entry,key为threadlocal实例,value是线程自己的值,每个线程都有自己的map,这里entry是一个弱引用,指向ThreadLocal实例,如果ThreadLocal实例没有强引用,只有一个弱引用指向它,那么垃圾回收线程就会回收这个ThreadLocal实例,这时候用entry.get()就返回null,也就可以回收对应的value了。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
用弱引用的好处是如果ThreadLocal实例如果没有强引用,只有弱引用,那么垃圾回收就会回收它,但是如果线程活着,垃圾回收器不会回收Entry,Value也会一直存在。那这里用弱引用其实就是为了做清理,相当于一个兜底,但是这个兜底还依赖对ThreadLocal对象 set() get() remove()方法的调用,可以是其他ThreadLocal对象,清理的是Thread拥有的map,如果不调用,就没有清理动作。可以看一下ThreadLocal#expungeStaleEntry()。
下面是一个可以导致内存溢出的代码:JVM参数: -Xmx64M -Xms32M,因为64位jvm默认开启指针压缩,因此大概在大于1500个线程之后就会OutOfMemory。
//如果threadlocal是局部变量,只get,new很多线程,能不能导致内存泄露呢?
public static void main(String[] args){
//持有线程对象,防止执行完之后线程被回收,threadlocalmap也会被回收
List<Thread> list = new ArrayList<>();
//1500就不会溢出,1600就会,40KB*1600 = 64MB,jvm运行参数-Xmx64M
for(int i = 0;i<1600;i++){
Thread tt = new Thread(() -> {
ThreadLocal<List<String>> localThreadLocal = new ThreadLocal<List<String>>(){
@Override
protected List<String> initialValue() {
//由于默认开启指针压缩,这里会占用40KB
return new ArrayList<>(10000);
}
};
List<String> aa = localThreadLocal.get();
aa.add(0,"1");
try {
//防止线程过早终止
Thread.sleep(5000L);
} catch (InterruptedException e) {
//throw new RuntimeException(e);
}
});
list.add(tt);
tt.start();
}
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
//throw new RuntimeException(e);
}
System.out.println(list.size());
}
运行输出:
Exception in thread "Thread-1532" Exception in thread "Thread-1535" Exception in thread "Thread-1534" java.lang.OutOfMemoryError: Java heap space
at java.util.ArrayList.<init>(ArrayList.java:153)
at ThreadLocalTest$2.initialValue(ThreadLocalTest.java:62)
at ThreadLocalTest$2.initialValue(ThreadLocalTest.java:58)
at java.lang.ThreadLocal.setInitialValue(ThreadLocal.java:180)
at java.lang.ThreadLocal.get(ThreadLocal.java:170)
at ThreadLocalTest.lambda$main$1(ThreadLocalTest.java:66)
at ThreadLocalTest$$Lambda$1/1747585824.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "Thread-1540" Exception in thread "Thread-1544" java.lang.OutOfMemoryError: Java heap space
结论
- 一般情况下,ThreadLocal都是private static final,用完之后一定remove,正常使用肯定不会内存泄露,如果没有remove,在线程池复用的情况下,可能会出很难排查的线上问题,因此要养成好习惯。
- 想要内存泄露,第一要大量线程长时间运行,第二就是ThreadLocal引用被设置为null,且后续在同一Thread实例的执行期间,没有发生对其他 ThreadLocal实例的get()、set()或remove()操作。只要存在一个针对任何ThreadLocal实例的get()、set()或remove()操作,就会触发Thread实例拥有的ThreadLocalMap的Key为null的Entry清理工作,释放掉 ThreadLocal弱引用为null的Entry。