ThreadLocal是解决线程安全的一项技术,今天我们来学习ThreadLocal的原理。
ThreadLocal到底是什么?
首先看看如何使用:
从上面结果看5个线程通过ThreadLocal拿到的变量互不影响。可以达到线程安全。
那么他是如何做到的呢?底层结构如何?
翻看ThreadLocal源码,并没有看到有存储数据的成员属性,那么数据存在哪里呢?
通过查看get方法源码,这个方法不用任何参数, 发现他是先获取当前线程对象,然后从当前线程对象获取一个ThreadLocalMap对象,然后从这个map对象把this(当前ThreadLocal对象)获取数据。
public T get() {
//获取当前线程对象
Thread t = Thread.currentThread();
//从线程对象获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//当前ThreadLocal对象,从map中查找值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//ThreadLocalMap还未初始化,创建一下并赋予初始值
return setInitialValue();
}
初始化ThreadLocalMap
private T setInitialValue() {
//获取初始化值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//创建ThreadLocalMap并初始化值
createMap(t, value);
return value;
}
初始化线程t的ThreadLocalMap,并赋值
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
接下来看下ThreadLocalMap在Thread中是怎么存放的。
public class Thread implements Runnable {
//线程成员变量,也就是每个线程都有个ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
///线程成员变量,这个后面再分析
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
从线程类源码看出,每个线程对象都会有一个ThreadLocalMap属性。
可以得出结论:ThreadLocal其实不存储数据,他是一个工具类,间接操作Thread对象中的ThreadLocalMap变量的。
既然数据都存在ThreadLocalMap,我们分析ThreadLocalMap的结构和底层实现。
首先理一下ThreadLocalMap和线程,还有ThreadLocal三者的关系
通过这个图可以看出Thread类中持有一个ThreadLocalMap引用,其实就是一个Entry类型的数组。Entry的key是ThreadLocal类型的,value 是Object 类型。也就是一个ThreadLocalMap可以持有多个ThreadLocal。
看下类图
捋一捋关系:
1、Thread持有ThreadLocalMap的引用,他们是1对1关系。
2、Entry是ThreadLocalMap的内部类,并且ThreadLocalMap持有Entry类型的数组。也就是一个ThreadLocalMap对应多个Entry。
3、ThreadLocal和ThreadLocalMap的关系是最难描述的,因为
ThreadLocalMap是ThreadLocal的子类,而ThreadLocalMap中存储的key类型是ThreadLocal,并且ThreadLocal是弱引用类型的。
看下他们的代码关系:
public class ThreadLocal<T> {
//内部类
static class ThreadLocalMap {
/**
* 存储数据的条目,key是WeakReference弱引用类型的ThreadLocal,
* key直接用WeakReference管理。
* 如果get方法(key==null)锁门条目不存在了,会主动清除,
* 避免内存泄漏的(分配的内存,没用了,但是没被回收)
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
为什么ThreadLocalMap设计成内部类?
主要是说明ThreadLocalMap 是一个线程本地的值,它所有的方法都是private 的,也就意味着除了ThreadLocal 这个类,其他类是不能操作ThreadLocalMap 中的任何方法的,这样就可以对其他类是透明的。同时这个类的权限是包级别的,也就意味着只有同一个包下面的类才能引用ThreadLocalMap 这个类,这也是Thread 为什么可以引用ThreadLocalMap 的原因,因为他们在同一个包下面。
虽然Thread 可以引用ThreadLocalMap,但是不能调用任何ThreadLocalMap 中的方法。这也就是我们平时都是通过ThreadLocal 来获取值和设置值。
这样设计的好处是什么?
ThreadLdocalMap 对使用者来说是透明的,可以当作空气,我们一直使用的都是ThreadLocal,这样的设计在使用的时候就显得简单,然后封装性又特别好。
set方法源码分析:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//操作ThreadLocalMap,设置数据,key是ThreadLocal对象。
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//第一次,将线程ThreadLocalMap初始化好。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
下面注意看ThreadLocalMap的set方法
private void set(ThreadLocal<?> key, Object value) {
//我们不像get()那样使用快速路径,因为使用set()创建新条目
//与替换现有条目至少一样普遍,在这种情况下,快速路径经常会失败。
Entry[] tab = table;
int len = tab.length;
//计算下标
//哈希魔数(增长数),也是带符号的32位整型值黄金分割值的取正
int i = key.threadLocalHashCode & (len-1);
//这里不断找下一个下标,直到找到数组下标位置为null的下标
//这里处理hash冲突,使用的是线性探测方法。
for (Entry e = tab[i];
e != null;
//线性探测方法 解决hash冲突
e = tab[i = nextIndex(i, len)]) {
//key
ThreadLocal<?> k = e.get();
//ThreadLocal找到了 替换旧值
if (k == key) {
e.value = value;
return;
}
//key已经被回收了
if (k == null) {
//陈旧数据替换 替换成本次新set的key,value
replaceStaleEntry(key, value, i);
return;
}
}
//构建新节点,存到下标i位置
tab[i] = new Entry(key, value);
int sz = ++size;
//是否要扩容了
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
//每次下标加1,线性查找
return ((i + 1 < len) ? i + 1 : 0);
}
看完上面的方法,就有疑问了,为什么ThreadLocalMap采用开放地址法来解决哈希冲突?
jdk 中大多数的Hash类都是采用了链地址法来解决hash冲突,为什么ThreadLocalMap 采用开放地址法来解决哈希冲突呢?首先我们来看看这两种不同的方式:
1、链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在这个链中进行。
2、开放地址法
这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。
链地址法和开放地址法的优缺点
开放地址法:
1、`容易产生堆积问题,不适于大规模的数据存储。`
2、散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。
3、删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。
链地址法:
1、处理冲突简单,且无堆积现象,平均查找长度短。
2、链表中的结点是动态申请的,适合构造表不能确定长度的情况。
3、删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
指针需要额外的空间,故当结点规模较小时,开放地址法较为节省空间。
ThreadLocalMap采用开放地址法原因
1、ThreadLocal类中看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table。
2、ThreadLocal 往往存放的数据量不会特别大(而且key是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低。
自动清理源码:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
//找到最小的一个被回收的下标 默认是staleSlot
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//找到了相同的key
if (k == key) {
e.value = value;
//交换位置,这个时候staleSlot位置上的变成新的有用的数据 i位置无用
//为什么要交换 不交换 的时候大坐标位置上存key 下次set会直接存入小下标位置 导致两个相同的key 出现数据错乱问题
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
没有找到无效的key
if (slotToExpunge == staleSlot)
//slotToExpunge 设置成无效
slotToExpunge = i;
// 回收slotToExpunge
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
// 更新slotToExpunge为最大需要回收的key
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
//这里设置为null ,方便让GC 回收
e.value = null;
tab[i] = null;
size--;
} else {
//这里主要的作用是由于采用了开放地址法,所以删除的元素是多个冲突元素中的一个,需要对后面的元素作
//处理,可以简单理解就是让后面的元素往前面移动
//为什么要这样做呢?主要是开放地址寻找元素的时候,遇到null 就停止寻找了,你前面k==null
//的时候已经设置entry为null了,不移动的话,那么后面的元素就永远访问不了了,下面会画图进行解释说明
int h = k.threadLocalHashCode & (len - 1);
//他们不相等,说明是经过hash 是有冲突的
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
//这个方法是从i 开始往后遍历(i++),寻找过期对象进行清除操作
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
// 用do while 语法,保证 do 里面的代码至少被执行一次
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
//如果遇到过期对象的时候,重新赋值n=len 也就是当前数组的长度
n = len;
removed = true;
//在一次调用expungeStaleEntry 来进行垃圾回收(只是帮助垃圾回收)
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);//无符号右移动一位,可以简单理解为除以2
return removed;
}
通过查看上面的源码,我们知道expungeStaleEntry() 方法是帮助垃圾回收的,我们还可以发现get和set方法都可能触发清理方法expungeStaleEntry(),所以正常情况下是不会有内存溢出的,但是如果我们没有调用get 和set 的时候就会可能面临着内存溢出,养成好习惯不再使用的时候调用remove(),加快垃圾回收,避免内存溢出,退一步说,就算我们没有调用get和set和remove方法,线程结束的时候,也就没有强引用再指向ThreadLocal中的ThreadLocalMap了,这样ThreadLocalMap 和里面的元素也会被回收掉,但是有一种危险是,如果线程是线程池的,在线程执行完代码的时候并没有结束,只是归还给线程池,这个时候ThreadLocalMap和里面的元素是不会回收掉的。
关于ThreadLocal的思考
ThreadLocal找到空key时候尝试清理一遍无效的entry,此时向前遍历是为了找到一个最小需要清理的entry下标。向后遍历是为了找到第一个相同key的下标,这里是为了解决key相同的时候,判断下标出错,有用的数据往前移动。
ThreadLocalMap的key设计为弱引用,可以起到标识key失效了,需要被回收,使用线性探测进行回收清理失效的数据。
ThreadLocal两种清除方式分开讨论
他们的应用场景不一样 remove方法,主动清除数据的机制。
而 set/get方法里的清理逻辑 是针对 ThreadLocal WeakReference.get=null 这个对象被回收了,value还存在的情况。
当一个ThreadLocal失去强引用,生命周期只能存活到下次gc前,此时ThreadLocalMap中就会出现key为null的Entry,当前线程无法结束,这些key为null的Entry的value就会一直存在一条强引用链,造成内存泄露。
解决方案:
建议将ThreadLocal变量定义成private static的,在调用ThreadLocal的get()、set()方法完成后,再调用remove()方法,手动删除不再需要的ThreadLocal。
InheritableThreadLocal 理解
InheritableThreadLocal是ThreadLocal的子类,作用是用来共享父类的ThreadLocal数据。使用方法和ThreadLocal一样,通过模版方法设计模式,重写了getMap,createMap。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
//获取线程的inheritableThreadLocals
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
//设置线程的inheritableThreadLocals
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
//创建线程的地方,会初始化inheritableThreadLocals变量
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
在Spring框架的web模块就用到了ThreadLocal和InheritableThreadLocal。用来对每个线程的请求Request属性进行存储。
public abstract class RequestContextHolder {
private static final boolean jsfPresent =
ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<>("Request context");
}
ThreadLocal在框架中使用的比较多,工作中也有可能用的到,实现线程间数据独占使用,保证线程安全,了解一些原理对工作中也有帮助。
欢迎大家关注我的公众号一起学技术,服务端技术栈。