目录一览
深入理解ThreadLocal
一、ThreadLocal简介
1.1 概念
ThreadLocal 听名知意,即是 线程变量,主要作用是用来存储线程本地变量,适用于隔离线程间变量,而在方法或类间共享的场景。
它可以理解成是一种以自身实例为键, 任意类型对象为值的键值对存储结构。这种数据结构可以被附带到每一个线程中,也就是说,一个线程都可以根据一个ThreadLocal
实例查询到绑定在自己身上的一个值;
每个Thread
都有自己的ThreadLocal
,且只能由当前Thread
使用,ThreadLocal
是每个线程独有的,在多个线程操作共享变量时,会出现线程安全问题,但如果一个变量是线程独有的,对于其他线程不可见的,各自操作各自的,自然不涉及线程安全问题;
可以通过声明一个任意泛型的ThreadLocal
对象,并在不同线程中使用它,就会在不同线程中创建多个实例副本,即同一个ThreadLocal
对象在不同线程中使用可以存储不同的值;
1.2 结构图
ThreadLocal
在每个线程内涉及的调用,包括方法、类中都是共享的,且不需要进行传参,可以理解为一个线程内部的全局变量,注意:只在一个线程中有效。
下图可以增强理解:
1.3 核心函数
方法声明 | 描述 |
---|---|
protected T initialValue() | 返回当前线程局部变量的初始值 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
二、ThreadLocal简单使用
2.1 测试代码
下面的例子中,分别声明了两个ThreadLocal
对象实例A、B,并初始化值。
在主线程中调用了ThreadLocal
的相关操作,同时穿插调用线程池执行任务,并在任务中调用ThreadLocal
,猜猜看执行结果是啥?
/**
* ThreadLocal相关测试
*
* @author Jiechong Bai
* @since 2023-09-06 22:08
*/
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ThreadLocalTest.class)
public class ThreadLocalTest {
/**
* 设置ThreadLocal初始化值
*/
private static final ThreadLocal<String> A = ThreadLocal.withInitial(() -> "A");
private static final ThreadLocal<String> B = ThreadLocal.withInitial(() -> "B");
@Test
public void run1() {
A.set("A-MAIN");
log.info("Main thread value is : {}", A.get());
ThreadPoolUtils.execute(() -> {
A.set("A-THREAD");
log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), A.get());
log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), B.get());
A.remove();
B.remove();
});
B.set("B-MAIN");
log.info("Main thread value is : {}", B.get());
ThreadPoolUtils.execute(() -> {
B.set("B-THREAD");
log.info("Child t2 {} thread value is : {}", Thread.currentThread().getName(), A.get());
log.info("Child t2 {} thread value is : {}", Thread.currentThread().getName(), B.get());
A.remove();
B.remove();
});
A.remove();
B.remove();
}
}
下面是运行后的结果:
//可以看到,主线程分别打印A-MAIN、B-MAIN;
Main thread value is : A-MAIN
Main thread value is : B-MAIN
//两次执行任务是两个新的线程Child t1、Child t2
//Child t1 打印 A-THREAD(手动set)、B(初始值);
Child t1 pool-1-thread-1 thread value is : A-THREAD
Child t1 pool-1-thread-1 thread value is : B
//Child t2 打印 A(初始值)、B-THREAD(手动set);
Child t2 pool-1-thread-2 thread value is : A
Child t2 pool-1-thread-2 thread value is : B-THREAD
2.2 分析执行流程
简单分析下上面例子的执行流程:
1、类加载时,会先加载静态常量ThreadLocal A
、ThreadLocal B
的初始值分别为"A"、“B”。
2、进入主线程,此时,可以理解为在主线程绑定的ThreadLocal
中,ThreadLocal A、B
的初始值分别是“A”、“B”,调用 A.set("A-MAIN")
,此时ThreadLocal A
的值由初始值 “A” 改变为 “A-MAIN”,此时ThreadLocal A、B
在 主线程中的值分别为 “A-MAIN”、“B”;
3、由于使用线程池异步执行,所以这里主线程大概率先执行完,依次打印出A-MAIN、B-MAIN,并执行remove()
;
4、进入子线程 t1中,此时,在子线程 t1 绑定的ThreadLocal
中, ThreadLocal A、B
的初始值分别是 “A”、“B”,调用 A.set("A-THREAD")
,此时ThreadLocal A 的值由初始值 “A” 改变为 “A-THREAD”,此时ThreadLocal A、B
在子线程 t1 中的值分别为 “A-THREAD”、“B”,打印结果后 执行remove()
;
5、进入子线程 t2中,此时,在子线程 t2 绑定的ThreadLocal
中, ThreadLocal A、B
的初始值分别是 “A”、“B”,调用 B.set("B-THREAD")
,此时ThreadLocal B
的值由初始值 “B” 改变为 “B-THREAD”,此时ThreadLocal A、B
在子线程 t2 中的值分别为 “A”、“B-THREAD”,打印结果后 执行remove()
;
根据流程并结合 1.3 的结构图来看,这里已经非常明显的看到ThreadLocal
的特性了,各个线程之间操作互不干扰,ThreadLocal
的值是互相独立的。
三、ThreadLocal实现原理
3.1 源码分析
下面我会把我认为比较核心的点单拎出来,并结合源码一行行分析
3.1.1 ThreadLocal.Class 结构
ThreadLocal
类中有两个静态内部类SuppliedThreadLocal
和ThreadLocalMap
, 其中ThreadLocalMap
就是ThreadLocal
机制的核心之一,非常重要,后面会说到。
除了两个静态内部类以外,还有一系列对外提供的方法,下面会列出核心函数来分析。
另外还有三个私有成员变量,都是ThreadLocalMap
用来存储内容时用到的,后面都会一一解读
3.1.2 ThreadLocalMap.Class(静态内部类)
static class ThreadLocalMap {
//(1) ThreadLocalMap的静态内部类
//(2) 键值对结构类Entry,继承了弱引用,意将为key的ThreadLocal对象设为弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
//(3) 键值对中的Value,在下面构造方法中被赋值
Object value;
//(4) 此构造供外部调用,用于创建一个全新的Entry节点
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//(5) Entry[] table 数组的初始容量为16
private static final int INITIAL_CAPACITY = 16;
//(6) 用来存储 Entry类型数组,可以看做是kv键值对的数组
private Entry[] table;
//(7) table数组长度
private int size = 0;
//(8) 可以理解为触发自动扩容的阈值,默认为0
private int threshold;
//(9) resize扩容阈值加载因子为2/3,也就是三分之二
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
//...此处省略ThreadLocalMap.Class的方法
}
ThreadLocalMap
是ThreadLocal
的一个静态内部类。每一个Thread
对象实例中都维护了一个ThreadLocalMap
对象(下面 3.1.3 会说到),它本质上是一组以ThreadLocal
为key,任意类型值为value的K-V键值对。
另外,ThreadLocalMap
内部还维护了一个Entry
静态内部类,该类继承了WeakReference
(弱引用,该类的引用在虚拟机进行GC时会被进行清理),并指定所引用的泛型为ThreadLocal
类型,Entry
是一个键值对结构,而真正的内容都在table中存储。
//一组以ThreadLocal为key,任意类型值为value的K-V键值对
private Entry[] table;
看到这里,是不是觉得ThreadLocalMap
有点HashMap
的味道了,整个ThreadLocal
中核心内容都是围绕ThreadLocalMap
进行操作,而ThreadLocalMap
的核心内容都是在围绕 Entry[] table
的数组键值对存储结构进行操作。
关于Thread、ThreadLocal、ThreadLocalMap
之间的关系,下图可以增强理解:
3.1.3 threadLocalHashCode 与 HASH_INCREMENT
首先说结论:HASH_INCREMENT
与threadLocalHashCode
是通过一个固定的Hash散列算法来决定ThreadLocal在数组中的实际存储下标,即是决定ThreadLocal在作为key存入Entry[] table的哪个下标,取值时同样也要用相同的算法求出下标,关于散列的作用这里不再过多阐述了。
HASH_INCREMENT
,翻译过来就是 增量哈希, 它是threadLocalHashCode
用来计算下一个哈希码时需要用到的魔数,源码中用十六进制表示,用转换器转换下就可以得出对应的十进制是 1640531527
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
官方对HASH_INCREMENT
的注释:
连续生成的哈希代码之间的差异 - 将隐式顺序线程本地 ID 转换为两大小表的近乎最佳分布的乘法哈希值。
我刚开始其实并不理解这个数字,直到在网上找到一些解答,最后才知道1640531527是在有符号的int范围内是黄金分割数;
在ThreadLocalMap的源码中的getEntry()
方法中可以看到,获取值时求下标的算法为:
keyIndex = key.threadLocalHashCode & (table.length - 1);
这个算法就可以简单看做是对当前table长度的取模,我看到这块的时候很眼熟,因为与HashMap中对散列的处理大差不差,这里就是通过threadLocalHashCode取模出一个位于当前长度中一个合适的下标, 之所以用位运算是因为&的效率要比%高得多,主要原因是位运算是直接操作内存的,不需要再转成十进制数据,因此处理速度快
不止getEntry()
中,其他有使用到threadLocalHashCode
的地方,不管是存入还是取值都是此算法,下面列出几处ThreadLocalMap的源码:
private Entry getEntry(ThreadLocal<?> key) {
//计算出下标,table.length 即 Entry[] table当前长度
int i = key.threadLocalHashCode & (table.length - 1);//散列算法
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
} else {
//计算出下标,table.length 即 Entry[] table当前长度
int h = k.threadLocalHashCode & (newLen - 1); //散列算法
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
照着源码中的方式,比葫芦画瓢,写一个在固定长度中求散列值的demo:
public class TestThreadLocalHashCode {
public static void main(String[] args) {
hashCode(4);
hashCode(16);
hashCode(32);
hashCode(64);
}
private static final int HASH_INCREMENT = 0x61c88647;
//每次调用相当于在一个ThreadLocalMap在固定长度下生成所有的下标
private static void hashCode(int capacity) {
final AtomicInteger nextHashCode = new AtomicInteger(HASH_INCREMENT);
for (int i = 0; i < capacity; i++) {
int keyIndex = nextHashCode.getAndAdd(HASH_INCREMENT) & (capacity - 1);
System.out.print(keyIndex + " ");
}
System.out.println();
}
}
在不触发二次扩容的场景下,每个ThreadLocalMap
中的长度分别为4,16,32,64,输出结果:
3 2 1 0 //4
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 //16
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 //32
7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0 //64
从打印的结果可以看出来,在散列后都刚好填满了整个数组长度,尽可能避免hash冲突,实现了完美散列,这样在使用中就尽可能的提高了在ThreadLocalMap中获取元素的命中率,尽可能提高效率。
3.1.4 与Thread.Class 的关联
点击内部类ThreadLocalMap
发现:
进入Thread
类中,可以看到类中声明了一个ThreadLocal.ThreadLocalMap threadLocals
的属性,除此之外还有一个相同类型的 ThreadLocal.ThreadLocalMap inheritableThreadLocals
,它们两个都是ThreadLocal.ThreadLocalMap
类型, 通过查看ThreadLocalMap
类得知,实际上它类似于一个HashMap
,在默认情况下,这两个变量的初始值都为null,如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
这里可以理解为,在一个线程中,通过声明出一个类型ThreadLocalMap
类型的键值对结构,从而实现 在一个线程上 绑定多个键值对内容,通过上一步查看ThreadLocalMap
类得知,key
为ThreadLocal
对象本身,value
为任意类型。
可能和一开始想的不一样,每个线程的本地变量不是存储在ThreadLocal
实例中,而是存储在调用线程实例中的threadLocals
变量中,也就是说,代码中ThreadLocal
类型的本地变量是存放在具体的线程上,那ThreadLocal
本身是不是就可以理解为一个类似工具类的存在?
到这里自然也理解了为何线程之间的操作相互隔离,互不影响,且不存在线程安全问题,本质上都是每个线程在基于不同ThreadLocal
作为key
,操作自己私有的threadLocals
变量。
3.1.5 初始化函数
ThreadLocal
提供两种初始化默认值的方式,不论哪种方式,都是通过子类化并重写initialValue()
方法来实现的。
- 方式一 :声明匿名内部类重写
ThreadLocal
的initialValue()
函数
//定义ThreadLocal时,使用匿名内部类的洗的写法
private static final ThreadLocal<String> t = new ThreadLocal<String>(){
@Override
protected String initialValue() {
//此处可以自定义默认值
return super.initialValue();
}
};
- 方式二 :静态内部类
SuppliedThreadLocal
继承自ThreadLocal
,并重写initialValue()
方法,加上函数式接口,lambda表达式初始化默认值;
private static final ThreadLocal<String> A = ThreadLocal.withInitial(() -> "A");
//Supplier为函数式接口,调用静态内部类SuppliedThreadLocal的构造方法初始化默认值
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
//继承ThreadLocal,并重写initialValue()方法
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
//函数式接口定义
private final Supplier<? extends T> supplier;
//构造函数
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
//实际调用
@Override
protected T initialValue() {
return supplier.get();
}
}
3.1.6 set()方法源码
public void set(T value) {
//(1)、获取当前线程(调用者线程)
Thread t = Thread.currentThread();
//(2)、以当前线程 t 作为getMap()参数,获取线程实例内的threadLocals变量
ThreadLocalMap map = getMap(t);
if (map != null)
//(3)、如果map不为null,则调用ThreadLocalMap的set()方法添加进Entry[] 数组
map.set(this, value);
else
//(4)、如果map为null,则说明线程内threadLocals变量为默认值null, 本次是首次添加;
//则将调用者线程、本地变量value作为参数,调用createMap创建map
createMap(t, value);
}
1处代码:Thread.currentThread()
即获取当前调用此方法的线程(调用者线程)
2处代码:getMap()
源码如下,可以看到直接返回调用者线程 t 的threadLocals变量。
ThreadLocalMap getMap(Thread t) {
//直接返回当前调用者线程 t 的threadLocals 变量
return t.threadLocals;
}
3处代码:如果getMap()
返回不为null
,则直接调用ThreadLocalMap.set()
将 this、本地变量value设置到threadLocals
中,这里的this是精髓,已知this即是当前ThreadLocal实例,是map的key,而value则是对应的本地变量。
4处代码:createMap()
源码如下:
void createMap(Thread t, T firstValue) {
//调用内部类ThreadLocalMap的构造方法进行初始化
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
当一个线程第一次调用createMap()
方法前,t.threadLocals == null
,这里直接new了一个ThreadLocalMap实例赋值给t.threadLocals
,调用的构造方法如下:
//见名知意,firstKey: 第一个key, firstValue:第一个value
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化Entry[] 类型数组,并赋予初始化长度 INITIAL_CAPACITY
table = new Entry[INITIAL_CAPACITY];
//根据当前ThreadLocal实例hash值与初始化容量做与运算,得出当前存储下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//真正存入Entry[] table的某个下标中
table[i] = new Entry(firstKey, firstValue);
//初始化长度为1
size = 1;
//这里应该是计算下一次自动扩容的长度
setThreshold(INITIAL_CAPACITY);
}
构造方法中初始化了ThreadLocalMap
相关信息,不仅创建了threadLocals
,同时也将 firstKey, firstValue
添加到了threadLocals
中;
所以这里可以得出,当一个线程第一次调用set(Object obj)
方法时,会创建此线程中的 threadLocals
变量,并将所引用的ThreadLocal
实例作为 firstKey
,本次set
的值作为firstValue
存入到此threadLocals
变量的Entry[] entry
数组键值对中去;
注意: 如果在线程中继续重复调用set()
方法,不会再走createMap()
方法, 因为此线程的threadLocals
变量在第一次调用时已经初始化过了,getMap(t)
返回不是null
,后续就是基于不同的ThreadLocal
实例作为key 存入 Entry[] entry
中。
当然,如果在一个线程中重复调用同一个ThreadLocal
实例的set()
方法,则是后者值覆盖前者值的情况,与HashMap一样;
下面是同一个ThreadLocal
实例值覆盖的代码示例以及debug截图:
private static final ThreadLocal<String> A = ThreadLocal.withInitial(() -> "A");
public void run1() {
ThreadPoolUtils.execute(() -> {
//重复设置 ThreadLocal A
A.set("A-THREAD");
A.set("A-THREAD-01");
log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), A.get());
log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), B.get());
A.remove();
B.remove();
});
}
3.1.7 get()方法源码
public T get() {
//(1)、获取当前线程(调用者线程)
Thread t = Thread.currentThread();
//(2)、以当前线程 t 作为getMap()参数,获取线程实例内的threadLocals变量
ThreadLocalMap map = getMap(t);
//(3)、如果调用者线程内部的threadLocals变量不为null
if (map != null) {
//(4)、调用getEntry(this)获取此key(ThreadlLocal实例)对应的value
ThreadLocalMap.Entry e = map.getEntry(this);
//(5)、如果获取到的Entry对象不为null,则返回e.value
if (e != null) {
@SuppressWarnings("unchecked") //类型强转去除警告
T result = (T)e.value;
return result;
}
}
//(6)、一旦3,5两步代码某一个为null,则会调用此方法创建map或者set值
return setInitialValue();
}
- 1、2、3处代码与
set()
方法一致,上面已经详细说过,这里不再阐述;
其中有趣的点在于3、5处代码的两个if判空:
-
3处代码如果为false,说明此线程所绑定的
threadLocals == null
,还没初始化过; -
5处代码如果为false,则说明
threadLocals
已经初始化过,只是内部的Entry[] table
还没有存入key为当前ThreadLocal
实例的内容;
二者不管谁为false,都会走到第6处代码,调用下面的setInitialValue()
方法。
private T setInitialValue() {
//(7)、调用initialValue()方法获取默认值
T value = initialValue();
//(8)、获取当前线程(调用者线程)
Thread t = Thread.currentThread();
//(9)、以当前线程 t 作为getMap()参数,获取线程实例内的threadLocals变量
ThreadLocalMap map = getMap(t);
//(10)、如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
if (map != null)
map.set(this, value);
else
//(11)、如果map为null,说明首次添加,需要首先创建出对应的map
createMap(t, value);
//(12)、返回value,即是初始化的默认值
return value;
}
- 7处代码:调用
initialValue()
方法获取默认值,此方法对应不同的初始化方式调用的方式也不一样,涉及到 3.1.4 中两种初始化函数中重写的initialValue()
方法,如果是使用这两种初始化方式,根据继承特性,则调用时分别对应到以下位置:
方式一:声明匿名内部类重写ThreadLocal
的initialValue()
函数
private static final ThreadLocal<String> t = new ThreadLocal<String>(){
//调用此处
@Override
protected String initialValue() {
return "initValue";
}
};
方式二:静态内部类SuppliedThreadLocal
继承自ThreadLocal
,并重写initialValue()
方法
private static final ThreadLocal<String> A = ThreadLocal.withInitial(() -> "initValue");
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
//调用此处
@Override
protected T initialValue() {
return supplier.get();
}
}
方式三:只调用new ThreadLocal()
,则无重写,直接调用ThreadLocal
类中的initialValue()
方法,返回null。
private static final ThreadLocal<String> t1 = new ThreadLocal<>();
//调用此处
protected T initialValue() {
return null;
}
-
8、9处代码:与
get()
一致,不再阐述; -
10、11处代码:对应上了
get()
方法中的5、3两处的if
判空,对应两种情况分别处理;如果map != null
对应5处代码,说明table
还没有存入key为当前ThreadLocal
实例的内容,则直接调用ThreadLocalMap.set()
赋值到table
数组中;如果map == null
则对应 3处代码,说明此线程所绑定的threadLocals == null
,还没初始化过,则调用createMap()
创建map,并将firstKey、firstValue
赋值到table
数组中; -
12处代码:最终返回value给调用者。
3.1.8 remove() 方法源码
此方法在 4.2 内存泄漏问题 中做解读
3.2 总结
通过阅读源码,我大致了解到ThreadLocal
本质只是一个工具类,它将自身Hash
值作为key
,并提供了initialValue()、set()、get()、remove()
等方法操作实际存放在线程Thread
实例内的 ThreadLocal.ThreadLocalMap threadLocals
变量;
同时通过对threadLocalHashCode的完美散列,尽可能的提高使用效率,其实关于散列这块的处理和HashMap
的hash方法有异曲同工之处,我在写的同时又去复习了一遍HashMap的相关原理。
而threadLocals
是基于ThreadLocal
的内部类ThreadLocalMap
,ThreadLocalMap
类中又提供了一个名为Entry
的内部类,算是套娃了。
除此之外,ThreadLocalMap
内部还提供了Entry
类型的table
数组用于存储键值对,以及一系列操作table
数组的方法,这些方法才是真正操作threadLocals
的角色。
可以理解为在ThreadLocalMap
外面封装了一层ThreadLocal
工具类,并且将ThreadLocal
作为key,表面上调用的都是ThreadLocal
提供的方法,实际操作threadLocals
都出自ThreadLocalMap
之手。
其中的精髓在于用类与类之间的组合使用,同时还用了一种十分巧妙的方式,常见的Thread.currentThread()
获取当前调用者线程 、以及调用getMap(this)、createMap(this, value)
等对this的巧妙使用,十分值得借鉴与学习。
四、使用ThreadLocal存在的问题
4.1 线程池问题
到目前为止,我个人所发现的问题,如果在线程池任务中使用ThreadLocal
需要注意的点,这里可以直接看我2.1测试代码的demo,其中就是使用线程池提交任务,并在任务中调用了ThreadLocal
。
由于线程池中是用线程复用的方式,在线程池中的线程,尤其是核心线程数,只要项目不挂,甚至永远不会结束,使用不当就会造成一些问题;重复调用例子如果不调用remove()
则一定会取到之前同一个线程的旧值(可以把线程池核心线程数设置为1,更容易看到效果)。
例如:Thread t1 在第1次循环中将自身的threadLocals中的 ThreadLocal A 设置值成了“A-THREAD”,且后续没有remove()
,然后在第5次循环中复用到了Thread t1,这时不需要进行任何处理通过 ThreadLocal A 调用 get()
拿到的就是“A-THREAD”,这肯定与预期的不相同,如下:
private static final ThreadLocal<String> A = ThreadLocal.withInitial(() -> "A");
private static final ThreadLocal<String> B = ThreadLocal.withInitial(() -> "B");
public void run1() {
ThreadPoolUtils.execute(() -> {
log.info("1、Child t1 {} thread value is : {}", Thread.currentThread().getName(), A.get()); //A
log.info("2、Child t1 {} thread value is : {}", Thread.currentThread().getName(), B.get()); //B
A.set("A-THREAD");
log.info("3、Child t1 {} thread value is : {}", Thread.currentThread().getName(), A.get()); //A-THREAD
log.info("4、Child t1 {} thread value is : {}", Thread.currentThread().getName(), B.get()); //B
});
}
//在runTest方法中循环调用run1(),不间断创建线程池任务
public void runTest() throws InterruptedException {
for (int i = 0; i < 100; i++) {
run1();
//主线程每隔1s调用一次
TimeUnit.SECONDS.sleep(1);
}
}
日志打印:
//线程 pool-1-thread-1 第一次输出
1、Child t1 pool-1-thread-1 thread value is : A
2、Child t1 pool-1-thread-1 thread value is : B
3、Child t1 pool-1-thread-1 thread value is : A-THREAD
4、Child t1 pool-1-thread-1 thread value is : B
//线程 pool-1-thread-1 第二次输出,可以看到差别
1、Child t1 pool-1-thread-1 thread value is : A-THREAD
2、Child t1 pool-1-thread-1 thread value is : B
3、Child t1 pool-1-thread-1 thread value is : A-THREAD
4、Child t1 pool-1-thread-1 thread value is : B
如果在操作完成后依次调用remove()方法则不会出现上面的情况,如下:
public void run1() {
ThreadPoolUtils.execute(() -> {
log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), A.get()); //A
log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), B.get()); //B
A.set("A-THREAD");
log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), A.get()); //A-THREAD
log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), B.get()); //B
//每次使用完调用remove方法,否则一定会造成取到旧值的情况
A.remove();
B.remove();
});
}
日志打印:
//线程 pool-1-thread-1 第一次输出
1、Child t1 pool-1-thread-1 thread value is : A
2、Child t1 pool-1-thread-1 thread value is : B
3、Child t1 pool-1-thread-1 thread value is : A-THREAD
4、Child t1 pool-1-thread-1 thread value is : B
//线程 pool-1-thread-1 第二次输出
1、Child t1 pool-1-thread-1 thread value is : A
2、Child t1 pool-1-thread-1 thread value is : B
3、Child t1 pool-1-thread-1 thread value is : A-THREAD
4、Child t1 pool-1-thread-1 thread value is : B
到这里我还在想着每次操作完之后调用remove()
,把目前的值清空不就可以了。确保线程下次复用的时候值是预期的,但是在线程池的任务中,怎么尽量确保remove()
方法会被调用呢?
- 使用finally
public void run1() {
ThreadPoolUtils.execute(() -> {
try {
A.set("A-THREAD");
A.set("A-THREAD-01");
log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), A.get()); //A-THREAD
log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), B.get()); //B
} finally {
A.remove();
B.remove();
}
});
}
- 使用线程池钩子函数
在线程池类ThreadPoolExecutor
中定义了钩子函数,可以在初始化或者任务执行完做特殊处理,在里面初始化ThreadLocal
。重写beforeExecute()
方法:
protected void beforeExecute(Thread t, Runnable r) { }
参考文章:在线程池中使用ThreadLocal,你必须要知道这一点
4.2 内存泄漏问题
ThreadLocalMap
的数据源头 Entry[] table
,以及Entry
类继承了WeakReference
(四种引用之一:弱引用),在JVM自动GC
的时候会对Key进行清理;
但是对于如果value是强引用类型,就需要手动调用remove()
,否则在GC清理掉的Key的引用后,因为value可能是强引用类型,则没有被清理;
在使用者层面并不会再使用此value的引用,因为使用者层面不可能通过一个null key
来找到value的,这样就会造成内存泄漏,即ThreadLocalMap
中的 Entry[] table
会存在key为null
,但是value不为null
的entry项。
所以请在使用完ThreadLocal后请务必调用remove()
方法,避免造成内存泄漏。
例如:
try {
//业务代码
} finally {
threadLocal.remove();
}
下面是remove()
方法的源码:
public void remove() {
//(1) 获取当前线程绑定的threadLocals
ThreadLocalMap m = getMap(Thread.currentThread());
//(2) 如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
if (m != null)
//(3) 如果map不为null,继续调用remove(this)方法
m.remove(this);
}
查看3处调用的remove(this)
方法:
private void remove(ThreadLocal<?> key) {
//(1) 创建新都tab数组,引用指向当前ThreadLocal对象中的table
Entry[] tab = table;
//(2) tab的长度
int len = tab.length;
//(3) 根据当前ThreadLocal对象的唯一threadLocalHashCode值并通过与操作
// 获取当前ThreadLocal对象在table中value所在的下标值i
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//(4) 对table进行遍历,如果Entry所对应的key为当前ThreadLocal对象,执行clear方法
if (e.get() == key) {
//(5) 将引用置为null
e.clear();
//(6) 清除过时的条目,将当前Entry删除后,会继续循环往下检查是否有key为null的节点,如果有则一并删除,防止内存泄漏。
expungeStaleEntry(i);
return;
}
}
}
查看clear()
方法,clear()
方法位于Reference
类中,由于Entry
继承了WeakReference
,此处的clear则是多态的体现
public void clear() {
this.referent = null;
}
五、项目使用场景
精力有限,这里后续会持续更新补出一些项目中实际的案例