目录
我们都知道,在多线程环境下访问同一个共享变量,可能会出现线程安全的问题,为了保证线程安全,我们往往会在访问这个共享变量的时候加锁,以达到同步的效果,如下图所示。
对共享变量加锁虽然能够保证线程的安全,但是却增加了开发人员对锁的使用技能,如果锁使用不当,则会导致死锁的问题。而ThreadLocal能够做到在创建变量后,每个线程对变量访问时访问的是线程自己的本地变量。
1. ThreadLocal是什么?
从名字我们就可以看到 ThreadLocal 叫做本地线程变量,意思是说,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。
2. ThreadLocal怎么用?
public class ThreadLocalDemo {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args){
//创建第一个线程
Thread threadA = new Thread(()->{
threadLocal.set("ThreadA:" + Thread.currentThread().getName());
System.out.println("线程A本地变量中的值为:" + threadLocal.get());
threadLocal.remove();
System.out.println("线程A删除本地变量后ThreadLocal中的值为:" + threadLocal.get());
});
//创建第二个线程
Thread threadB = new Thread(()->{
threadLocal.set("ThreadB:" + Thread.currentThread().getName());
System.out.println("线程B本地变量中的值为:" + threadLocal.get());
System.out.println("线程B没有删除本地变量:" + threadLocal.get());
});
//启动线程A和线程B
threadA.start();
threadB.start();
}
}
ThreadLocal通常被声明为private static:
- 私有性(Private):使用private修饰符可以确保ThreadLocal实例不会被外部类访问,从而防止了其值被意外修改,增强了封装性。当然是否使用private修饰是一个普遍的问题而不是与ThreadLocal有关的一个具体问题。
- 静态性(Static):ThreadLocal通常会被声明为static,这样做的好处是可以避免重复创建与线程相关的变量(Thread Specific Object,TSO)。如果ThreadLocal被声明为某个类的实例变量(而不是静态变量),那么每创建一个该类的实例就会导致一个新的TSO实例被创建。这样,同一个线程可能会访问到同一个TSO(指类)的不同实例,这即便不会导致错误,也会导致浪费(重复创建等同的对象)
3. ThreadLocal源码分析
首先,我们看下Thread类的源码,如下所示:
由Thread类的源码可以看出,在Thread类中存在成员变量threadLocals和inheritableThreadLocals,这两个成员变量都是ThreadLocalMap类型的变量,而且二者的初始值都为null。只有当前线程第一次调用ThreadLocal的set()方法或者get()方法时才会实例化变量。
这里需要注意的是:每个线程的本地变量不是存放在ThreadLocal实例里面的,而是存放在调用线程Thread对象的threadLocals变量里面的。
也就是说,调用ThreadLocal的set()方法存储的本地变量是存放在具体线程的内存空间中的,而ThreadLocal类只是提供了set()和get()方法来存储和读取本地变量的值,当调用ThreadLocal类的set()方法时,把要存储的值放入调用线程Thread对象的threadLocals中存储起来,当调用ThreadLocal类的get()方法时,从当前线程的threadLocals变量中将存储的值取出来。
接下来,我们分析下ThreadLocal类的set()、get()和remove()方法的实现逻辑。
3.1set方法
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取Thread对象中的ThreadLocalMap成员属性
ThreadLocalMap map = getMap(t);
//获取的ThreadLocalMap对象不为空
if (map != null)
//设置value的值
map.set(this, value);
else
//获取的ThreadLocalMap对象为空,创建Thread类中的threadLocals变量
createMap(t, value);
}
如果调用getMap(t)方法返回的对象为空,则程序调用createMap(t, value)方法来实例化Thread类的threadLocals成员变量。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
也就是创建当前线程的threadLocals变量,并且存了一个key为Threadlocal对象,value为firstValue的键值对。
因为每个线程Thread对象的ThreadLocal.ThreadLocalMap threadLocals不同,所以即便是使用同一个key,但Map都不同了,拿出来的value自然也是相互隔离的。
我们再仔细看看 ThreadLocalMap 的 set() 方法:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
第一种情况: 通过hash计算后的槽位对应的Entry数据为空,直接将数据放到该槽位
第二种情况: 槽位数据不为空,key值与当前ThreadLocal通过hash计算获取的key值一致,直接更新该槽位的数据
第三种情况: 槽位数据不为空,往后遍历过程中,在找到Entry为null的槽位之前,没有遇到key过期的Entry
遍历散列数组,线性往后查找,如果找到Entry为null的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了key 值相等的数据,直接更新即可。
第四种情况: 槽位数据不为空,往后遍历过程中,在找到Entry为null的槽位之前,遇到key过期的Entry,如下图,往后遍历过程中,遇到了index=7的槽位数据Entry的key=null
散列数组下标为 7 位置对应的Entry数据key为null,表明此数据key值已经被垃圾回收掉了,此时就会执行replaceStaleEntry()方法,该方法含义是替换过期数据的逻辑,以index=7位起点开始遍历,进行探测式数据清理工作。
3.2get()方法
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的threadLocals成员变量
ThreadLocalMap map = getMap(t);
//获取的threadLocals变量不为空
if (map != null) {
//返回本地变量对应的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//初始化threadLocals成员变量的值
return setInitialValue();
}
通过当前线程来获取threadLocals成员变量,如果threadLocals成员变量不为空,则直接返回当前线程绑定的本地变量,否则调用setInitialValue()方法初始化threadLocals成员变量的值。
private T setInitialValue() {
//调用初始化Value的方法
T value = initialValue();
Thread t = Thread.currentThread();
//根据当前线程获取threadLocals成员变量
ThreadLocalMap map = getMap(t);
if (map != null)
//threadLocals不为空,则设置value值
map.set(this, value);
else
//threadLocals为空,创建threadLocals变量
createMap(t, value);
return value;
}
其中,initialValue()方法的源码如下所示
protected T initialValue() {
return null;
}
通过initialValue()方法的源码可以看出,这个方法可以由子类覆写,在ThreadLocal类中,这个方法直接返回null。
3.3remove()方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
可以看到,在remove()方法中,首先根据当前线程获取ThreadLocalMap类型的m对象,不为空,则直接调用m对象的有参remove()方法移除value的值。
我们继续往里面看
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.refersTo(key)) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
可以看到,在有参remove()方法中,会通过threadLocalHashCode计算出Entry对象在Entry数组中的位置,并获取出对应的Entry对象,如果Entry对象不为空,并且Entry对象中的Key等于传入的ThreadLocal对象,则清除对应的Key,并且调用expungeStaleEntry()方法。
接下来,我们再分析下expungeStaleEntry()方法
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
可以看到,在expungeStaleEntry()方法中,会将key为null(即ThreadLocal为null)对应的value设置为null,同时会把对应的Entry对象也设置为null,并且会将所有ThreadLocal对应的value为null的Entry对象设置为null,这样就去除了强引用,便于后续的GC进行自动垃圾回收,也就避免了内存泄露的问题。
注意:在ThreadLocal中,不仅仅是remove()方法会调用expungeStaleEntry()方法,在set()方法和get()方法中也可能会调用expungeStaleEntry()方法来清理数据。
ThreadLocal虽然提供了避免内存泄露的方法,但是ThreadLocal不会主动去执行这些方法,需要我们在使用完ThreadLocal对象中保存的数据后,在finally{}代码块中调用ThreadLocal的remove()方法,加快GC自动垃圾回收,避免内存泄露。
4.为什么key使用弱引用?
如果使用强引用,当ThreadLocal 对象的引用(强引用)被回收了,ThreadLocalMap本身(Entry数组)依然还持有ThreadLocal对象的强引用,如果没有手动删除这个key ,则ThreadLocal对象不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收, 可以认为这导致Entry内存泄漏。
如果使用弱引用,那指向ThreadLocal对象的引用就两个:ThreadLocalRef强引用和ThreadLocalMap中Entry的弱引用。一旦ThreadLocalRef强引用被回收,则指向ThreadLocal的就只有弱引用了,在下次gc的时候,这个ThreadLocal对象就会被回收。而这个弱引用Key也将置为null。
此时,我们可以看到,Entry对象中的Key,也就是ThreadLocal对象可以被GC自动回收,但是对应的value还在被引用,并且value是强引用,所以,value是不能被GC自动回收的,这种情况下就会存在内存泄露的风险。
使用了线程池,可以达到“线程复用”的效果。但是归还线程之前记得清除ThreadLocalMap,要不然再取出该线程的时候,ThreadLocal变量还会存在。这就不仅仅是内存泄露的问题了,整个业务逻辑都可能会出错。
5.ThreadLocalMap 和 HashMap 区别
ThreadLocalMap 和HashMap的功能类似,但是实现上却有很大的不同:
- HashMap 的数据结构是数组+链表,HashMap 是通过链地址法解决hash 冲突的问题,HashMap 里面的Entry 内部类的引用都是强引用。
- ThreadLocalMap的数据结构仅仅是数组,ThreadLocalMap 是通过开放定址法——线性探测来解决hash 冲突的问题,ThreadLocalMap里面的Entry 内部类中的key 是弱引用,value 是强引用。
如上图所示,如果我们插入一个value=27的数据,通过 hash 计算后应该落入槽位 4 中,而槽位 4 已经有了 Entry 数据。此时就会线性向后查找,一直找到 Entry 为 null 的槽位才会停止查找,将当前元素放入此槽位中。
6.ThreadLocal变量不具有传递性
使用ThreadLocal存储本地变量不具有传递性,也就是说,同一个ThreadLocal在父线程中设置值后,在子线程中是无法获取到这个值的,这个现象说明ThreadLocal中存储的本地变量不具有传递性。
代码示例:
那有没有办法在子线程中获取到主线程设置的值呢?此时,我们可以使用InheritableThreadLocal来解决这个问题。
7.InheritableThreadLocal使用示例
InheritableThreadLocal类继承自ThreadLocal类,它能够让子线程访问到在父线程中设置的本地变量的值