首先,看一张整体的结构图,来帮助理解
什么是ThreadLocal
ThreadLocal用于创建线程局部变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题
ThreadLocal的简单使用
package test;
public class ThreadLocalTest {
static ThreadLocal<String> threadLocal= new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
//设置线程1的本地变量的值
threadLocal.set("线程1");
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println("after remove : " + threadLocal.get());
});
Thread t2 = new Thread(()->{
//设置线程2的本地变量
threadLocal.set("线程2");
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println("after remove : " + threadLocal.get());
});
t1.start();
t2.start();
}
}
输出结果:
由此可知,虽然threadLocal为成员变量,即线程共享,但使用threadLocal在不用线程进行赋值、获取等操作时,不同线程的操作互不相扰。
那么ThreadLocal是如何作为成员变量,却能在实现多线程下数据不共享,作为线程局部变量?
ThreadLocal的原理
回到一开始的图:
ThreadLocal
并不实际存储数据,而是作为一个工具类,提供get、set方法根据当前线程用于操作当前线程中的实际数据- 每个
Thread
(线程)中都持有一个ThreadLocal.ThreadLocalMap
类型的成员变量,初始值为null。ThreadLocal的get、set方法实际上就是在操作这个成员变量。 ThreadLocalMap
持有一个Entry[]
类型的成员变量table,类似JDK1.7的HashMap中的Entry[] table
可以类比学习Entry
key为ThreadLocal<?>
类型的的弱引用,value为Object
类型的强引用
ThreadLocal的set()
public void set(T value) {
//1、获取当前线程(调用者线程)
Thread t = Thread.currentThread();
//2、获取当前线程的ThreadLocalMap变量
ThreadLocalMap map = getMap(t);
//3、如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值
if (map != null)
map.set(this, value);
//4、如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
//获取线程的threadLocals
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
//创建线程的threadLocals, this为ThreadLocal<?>的引用
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal的get()
在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。
引用自:https://www.cnblogs.com/fsmly/p/11020641.html#_label0
public T get() {
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取当前线程的threadLocals变量
ThreadLocalMap map = getMap(t);
//3、如果threadLocals变量不为null,就可以在map中查找到本地变量的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//4、执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
return setInitialValue();
}
private T setInitialValue() {
//protected T initialValue() {return null;}
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
if (map != null)
map.set(this, value);
//如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
return value;
}
ThreadLocal操作总结
根据上述源码的分析可知:ThreadLocal最终操作的都是调用线程的ThreadLocalMap成员变量,因此不同线程使用同一个ThreadLocal成员变量互不相扰。
每个线程内部有一个ThreadLocal.ThreadLocalMap
类型threadLocals
的成员变量,该变量的类型为类似于HashMap,其中的key为当前定义的ThreadLocal变量的this引用,value为使用set方法设置的值。每个线程的本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。
ThreadLocal使用不当造成内存泄漏问题
弱引用
弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null
ThreadLocalMap内部实际上是一个Entry数组:
Entry
是继承自WeakReference
(弱引用)的一个类。Entry
的key是指向ThreadLocal
的弱引用,value一般为强引用
当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录(Entry
),这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。
如果当前线程一直存在且没有调用该ThreadLocal的remove方法,此时存在两种情况:
- ThreadLocalMap之外存在ThreadLocal的引用:那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,无法进行垃圾回收,导致这些本地变量一直存在,可能会出现内存溢出
- ThreadLocalMap之外不存在ThreadLocal的引用:因为ThreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,所以下一次垃圾回收时ThreadLocal(key)将被回收。此时ThreadLocalMap中就可能存在key为null但是value不为null的现象,出现内存泄漏。
因此每次使用完ThreadLocal,建议调用它的remove()方法,清除数据,避免内存泄漏
内存泄漏问题总结
ThreadLocalMap
中的Entry
的key使用的是ThreadLocal
对象的弱引用,在没有其他地方对ThreadLocal
存在依赖时,ThreadLocalMap
中的ThreadLocal
对象(即key)就会被回收,而如果其他地方存在ThreadLocal
的引用则不会被回收。当key被回收时,Map中就可能存在key为null但是value不为null的现象,出现内存泄漏。
因此每次使用完ThreadLocal,建议调用它的remove()方法,清除数据,避免内存泄漏。