解剖ThreadLocal
提供线程内的局部变量,不同线程之间不会互相干扰,这种变量在线程的生命周期内作用,减少同一个线程内多个方法或组件之间一些公共变量传递的复杂度。
1.1、常用方法
| 方法声明 | 描述 |
|---|---|
ThreadLocal() |
创建threadLocal对象 |
public void set(T value) |
设置当前线程绑定的局部变量 |
public T get() |
获取当前线程绑定的局部变量 |
public void remove() |
移除当前线程绑定的局部变量 |
1.2、应用场景
-
Spring多数据源配置的切换。
-
Spring事务注解的实现。
-
如果开发者希望将类的某个静态变量与线程状态关联,可以考虑使用ThreadLocal。ThreadLocal的设计本身就是为了能够在当前线程中有属于自己的变量,并不是为了解决并发或者共享变量的问题。
1.3、ThreadLocal与Synchronized关键字
| synchronized | ThreadLocal | |
|---|---|---|
| 原理 | 同步机制采用以时间换空间的方式,只提供了一份变量,让不同的线程排队访问。 | ThreadLocal采用以空间换时间的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而互不干扰。 |
| 侧重点 | 多个线程之间访问资源同步。 | 多线程中让每个线程之间的数据相互隔离。 |
1.4、内部结构
-
早期的设计:每个ThreadLocal都创建一个ThreadLocalMap,线程作为map的key,线程的私有变量作为map的value。

-
当前的设计:在JDK8中,每个线程中维护一个ThreadLocalMap,ThreadLocal实例作为map的key,线程的私有变量作为map的value。

具体过程:
-
每个Thread线程内部都有一个ThreadLocalMap。
-
map里key为ThreadLocal对象,value是变量值。
-
Thread内部的map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程私有变量值。
-
对于不同的线程,每次获取value(也就是变量值),别的线程并不能获取当前线程的变量值,形成了变量的隔离,互不干扰。
-
对比图:

如今设计的好处:
-
每个ThreadLocalMap存储的Entry数量变少。
-
当线程销毁的时候,ThreadLocalMap也会随之销毁,减少内存的使用。(之前以Thread为key会导致ThreadLocalMap的生命周期很长)
1.4.1、小结
-
早期版本中ThreadLocalMap是由ThreadLocal维护的,map中key为线程实例,value为线程的私有变量;
-
现在的版本中ThreadLocalMap是由线程维护的,map中的key为ThreadLocal实例,value为线程的私有变量。
-
这样的设计既减少了ThreadLocalMap存储的Entry的数量,又因为ThreadLocalMap会随着与之相关的线程销毁而销毁,而解决了原本因为过长生命周期的ThreadLocalMap占用内存的开销。
1.5、源码分析
1.5.1、ThreadLocal源码
-
initialValue()方法:protected T initialValue() {
return null;
} -
setInitialValue()方法:private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
} -
createMap(Thread t, T firstValue)方法:void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
} -
getMap(Thread t)方法:ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
} -
set(T value)方法:public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
} -
get()方法:ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
} -
remove()方法:public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
1.5.2、ThreadLocalMap源码
ThreadLocalMap是ThreadLocal的静态内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现的。

1.6、弱引用和内存泄漏
1.6.1、术语
-
内存溢出(OOM):是指程序在申请内存时,没有足够的内存空间供其使用;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
-
内存泄漏:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,最终导致内存溢出。
-
强引用:使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
-
弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
1.6.2、分析

ThreadLocal自身并不储存值,而是作为一个key来让线程从ThreadLocal获取value(就是从对应的key中取出value)。Entry是中的key是弱引用,所以jvm在垃圾回收时如果外部没有强引用来引用它,ThreadLocal必然会被回收(key被回收)。但是,作为ThreadLocalMap的key,ThreadLocal被回收后,ThreadLocalMap就会存在value不为null的Entry。若当前线程一直不结束,可能是作为线程池中的一员,线程结束后不被销毁,就可能引发内存泄漏。因此,key弱引用并不是导致内存泄漏的原因,而是因为ThreadLocalMap的生命周期需要当前线程销毁才结束,并且没有手动删除对应value。
Entry中的key设为弱引用的原因:设置为弱引用的key能预防大多数内存泄漏的情况。如果key 使用强引用,引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。如果key为弱引用,引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被GC回收。value在下一次ThreadLocalMap调用set、get、remove的时候就会被清除。
使用ThreadLocal发生内存泄漏的前提是:
-
没有手动删除Entry。
-
线程依然保持在的情况。
根本原因就是ThreadLocalMap和Thread一样长,Thread依然存在且没有手动删除Entry中对应的key就会导致内存泄漏。
1.6.3、避免方式
避免ThreadLocal造成内存泄漏就需要在使用完之后调用remove()方法删除对应的Entry。
本文来自博客园,作者:是老胡啊,转载请注明原文链接:https://www.cnblogs.com/solar-9527/p/15103857.html

浙公网安备 33010602011771号