ThreadLocal可能大家都有耳闻,线程局部变量-同样的ThreadLocal对象在不同线程中获取到的值不同,用于并发场景确保线程安全使用。
那么它是如何实现线程局部变量并确保线程安全的呢,我们一起来看下ThreadLocal的底层实现。
底层实现
话不多说,直接上代码,一起来看看ThreadLocal的核心代码
get方法
//通过ThreadLocal获取我们需要的线程局部变量的方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map =this.getMap(t);
if (map !=null) {
ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
if (e !=null) {
T result = e.value;
return result;
}
}
return this.setInitialValue();
}
从代码来看,ThreadLocal的get方法中尝试从当前线程Thread对象中的ThreadLocalMap中,取出以自身为key的value并返回,如果尝试失败,即不存在对应的value值,则调用setInitialValue方法并返回其返回值,具体流程图如下:
因此我们可以看出,真正的线程局部变量是存储在Thread对象中的,ThreadLocal只是一个key,在不同的线程中,用同一个key能取出不同的value,是因为不同线程中的map是线程独有的。
而setInitialValue方法,看命名是初始化value的方法,不过这个方法的调用时机却是在get中取不到value时调用,可以看成是get方法的兜底策略,也可以看成是这个初始化方法是个懒加载,需要的之后才执行,接下来我们来看看这个兜底方法。
setinitialValue方法
private T setInitialValue() {
T value =this.initialValue();
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map =this.getMap(t);
if (map !=null) {
map.set(this, value);
} else {
this.createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal)this);
}
return value;
}
可以看到setInitialValue方法中,调用了initialValue方法,获取到初始的value,然后以自身为key,将初始的value写入当前线程的ThreadLocalMap中,所以initialValue方法才是初始值的真正来源,使用方可以根据需要自定义initialValue方法。
除此之外,ThreadLocal中还有两个较为重要的方法,set和remove方法
set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map =this.getMap(t);
if (map !=null) {
map.set(this, value);
} else {
this.createMap(t, value);
}
}
通过该方法可指定value并更新写入到当前线程的ThreadLocalMap中
remove方法
public void remove() {
ThreadLocal.ThreadLocalMap m =this.getMap(Thread.currentThread());
if (m !=null) {
m.remove(this);
}
}
该方法比较简单,就是从当前线程中的ThreadLocalMap中将ThreadLocal自身为key的键值对清理掉。
但是该方法的应用算是一个ThreadLocal的小进阶,会涉及到内存泄漏的问题,这一点我们在后面会讲到。
模型结构
通过对ThreadLocal源码的分析,我们已经知道了真正的线程局部变量是存储在每个线程的Thread对象中的ThreadLocalMap中,而这个map是以Thread为key,所以,同一个ThreadLocal在不同的线程中能拿到不同的值。整体模型如下:
ThreadLocal对象存在于堆中,线程共享,而在不同的线程Thread对象中的ThreadLocalMap,相同的ThreadLocal对应不同的value,而value才是我们所说的线程局部变量。
ThreadLocal的使用
public class ThreadMesHolder {
//设置线程局部变量初始值
private static ThreadLocalthreadMes =new ThreadLocal<>(){
@Override
protected StringinitialValue(){
return "hello world";
}
};
//获取线程局部变量
public static StringgetMes(){
return threadMes.get();
}
//设置线程局部变量
public static void setMes(String newMes){
threadMes.set(newMes);
}
//释放线程局部变量
public static void clear(){
threadMes.remove();
}
}
关于ThreadLocal的使用,这里写了一个简单的demo进行举例,demo中包含了对线程局部变量的获取、设置和清除操作,其中线程局部变量的清除经常会被忽视,部分场景下忘记清理使用后的局部变量可能会导致内存泄漏。我们来看看为什么会不及时调用ThreadLocal的remove方法会发生内存泄漏问题:
static class Entry extends WeakReference> {
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
this.value = v;
}
}
上面是ThreadLocalMap中entry的定义,可以看到在Entry中,key是用的弱引用类型,而弱引用的类型的回收时机是下一次垃圾回收,所以在线程使用完局部变量之后,如果没有及时调用remove方法对线程局部变量进行引用的释放,那么Entry中的key可能会被GC回收掉,而ThreadLocalMap中的value使用的是强引用不会被GC回收,那么此时Entry中只剩下value且对应key的引用被回收,无法通过key进行value引用的释放,在线程结束之前由于强引用的关系,该value一直不会被回收直到线程结束,从而导致内存泄漏。所以,在生命周期较长的线程中,对ThreadLocal的使用需要注意及时的调用remove方法释放局部变量的引用,避免内存泄漏问题。
//22.03.13更新
看代码的时候发现在ThreadLocal中有个方法expungeStaleEntry,该方法用于释放key为null的entry,调用场景为remove、replaceStaleEntry、cleanSomeSlots、getEntryAfterMiss、rehash这些方法被调用的时候,优化了内存泄漏问题