1.什么是ThreadLocal
ThreadLocal,线程本地化, 可以私有化存储线程的变量值。可以简单理解为ThreadLoacl能为每个线程提供一个专属于自己的存储空间, 可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。
常见用法包括:
- 存储单个线程上下文信息。比如存储id等;
- 使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;
- 减少参数传递。比如做一个trace工具,能够输出工程从开始到结束的整个一次处理过程中所有的信息,从而方便debug。由于需要在工程各处随时取用,可放入ThreadLocal。
2.实现原理
在Thread的类中,会有一个属性Map (java.lang.Thread#threadLocals),
每当我们调用ThreadLoacl的set/get方法,其实就是在当前线程对应的Map中,以ThreadLoacl为key,存或者取对应的value。
代码示例:
public static void main(String[] args) throws InterruptedException {
ThreadLocal<Integer> tl1 = new ThreadLocal<>();
tl1.set(1);
ThreadLocal<String> tl2 = new ThreadLocal<>();
tl2.set("main");
System.out.println("main线程:"+tl1.get());
System.out.println("main线程:"+tl2.get());
new Thread(()->{
tl1.set(2);
tl2.set("thread");
System.out.println("线程2:"+tl1.get());
System.out.println("线程2:"+tl2.get());
}).start();
Thread.sleep(100L);
System.out.println("main线程睡醒后:"+tl1.get());
System.out.println("main线程睡醒后:"+tl2.get());
}
运行结果:
可以看到虽然,两个线程对同一个ThreadMap对象做了操作,但是没有相互影响。
3.源码解析:
看看ThreadLocal的源码,是否和我们刚刚说的实现原理一致
ThreadLocal的set方法源码 :
//源码中的set方法
//java.lang.ThreadLocal#set()
public void set(T value) {
//1.获取当前操作这个ThreadLoacl的线程
Thread t = Thread.currentThread();
//2.获取当前线程的属性:map
ThreadLocalMap map = getMap(t);
if (map != null)
//3—1.非空,直接对map进行设置
//此时key为threadLoacl的实例对象,value为传值
map.set(this, value);
else
//3-2.空,调用创建方法,同时设值
createMap(t, value);
}
//获取线程中的属性:java.lang.Thread#threadLocals
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal的get方法源码 :
//源码中的get方法
//java.lang.ThreadLocal#get()
public T get() {
//1.获取当前操作这个ThreadLoacl的线程
Thread t = Thread.currentThread();
//2.获取当前线程的属性:map
ThreadLocalMap map = getMap(t);
if (map != null) {
//3-1.map不为空,直接调用get方法获取value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//3-2.为空 调用创建方法,并返回null
return setInitialValue();
}
private T setInitialValue() {
//此方法返回null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
可以看到ThreadLocal的实现源码,符合我们前文所描述的实现原理。
4.ThreadLoacl与内存泄漏问题
一句话概括: 本该回收的无用对象没有得到回收 。
简而言之,jvm中有一些对象已经属于无用的垃圾了,但是但是由于某些原因,导致gc无法清除它。
ThreadLoacl可能会造成内存泄漏的原因:
我们将ThreadLoacl本身作为key。存放在Thread的Map中,那么在此线程的存活时间内,该ThreadLoacl对象都会被该线程的Map所持有,那么即使这ThreadLoacl对象不在使用了,gc也不会对该对象进行回收,所以很容易就造成了内存泄漏。
ThreadLoacl对内存泄漏的自身解决方法
刚刚提到的问题ThreadLoacl也想到了,所以我们看ThreadLoacl.ThreadLocalMap的内部实现:
可以看到,该Map的Entry继承了WeakReference,它是一个弱引用
WeakReference如字面意思,弱引用。那么也就是说,ThreadLocalMap中保存的Entry.key实际上是一个弱引用对象(这句话可能需要对map源码熟悉的人才能看懂),而弱引用对象,其所指向的对象在GC的时候是会被回收掉。 (详见:java四种引用类型详解)
结合上文实例代码分析
-
如果使用强引用:,当ThreadLocal对象(假设为ThreadLocal@123)的引用(即:tl1,是一个强引用,指向ThreadLocal@123)被回收了,ThreadLocalMap本身依然还持有ThreadLocal@123的强引用,如果没有手动删除这个key,则ThreadLocal@123不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收,可以认为这导致Entry内存泄漏。
-
如果使用弱引用,那指向ThreadLocal@123对象的引用就两个:tl1强引用,和ThreadLocalMap.Entry 中key的弱引用。一旦tl1被回收,则指向ThreadLocal@123的就只有弱引用了,在下次gc的时候,这个ThreadLocal@123就会被回收。
看到这里我们还会有疑问,ThreadLocalMap.Entry的key对象被回收了,此时key变成了null,但是我们是没法访问这些Entry的,但由于Entry中还保存着value,value可能还占用着一大块内存,那么这部分内存就相当于内存泄漏了,因为他占用着内存没法回收,而我们又没法访问到它。
对此在调用ThreadLocal的处理方法时,在调用get()/set()/remove()方法的时候都会清除线程ThreadLocalMap里所有key为null的value。这些举动不能保证内存就一定会回收,因为可能这条线程被放回到线程池里后再也没有使用,或者使用的时候没有调用其get()/set()/remove()方法。
所以我们日常使用时要注意,在 ThreadLocalMap 已经确定不需要在使用时,可以手动调用 remove()
方法。
ThreadLoacl可能内存泄漏归根结底是由于ThreadLocalMap的生命周期跟Thread一样长。