Thread Local的学习笔记
Thread Local的学习笔记
在多线程编程中,如何安全、简洁地为每个线程维护独立的变量副本,是开发者经常面临的挑战。Java 提供的 ThreadLocal
能够让我们轻松地在不同线程间隔离数据,避免了使用 synchronized
或复杂上下文传递的麻烦。本文将从使用方法、底层实现、常见陷阱与最佳实践几方面,深度剖析 ThreadLocal
的原理与应用,帮助你在日常开发中驾轻就熟地运用它。
一、Thread Local怎么使用?
线程局部变量
是一种用于实现线程局部变量的工具类。它允许每个线程都拥有自己的独立副本,从而实现线程隔离。每个线程各用各的
怎么使用Thread Local?
创建Thread Local
1
2//创建一个ThreadLocal变量
public static ThreadLocal<String> localVariable = new ThreadLocal<>();设置Thread Local的值
1
2//设置ThreadLocal变量的值
localVariable.set("天下万般兵刃,唯有过往伤人最深");获取Thread Local的值
1
2//获取ThreadLocal变量的值
String value = localVariable.get();删除Thread Local的值
1
2//删除ThreadLocal变量的值
localVariable.remove();记得用完要及时删除啊。至于原因,往下看你慢慢就知道了。
OK,到了这里,Thread Local的基本使用你就已经学会了。往下看就是更深入的一些知识了。当然,个人水平有限,也是一个初学者,这个博客也只是记录了自己的一个学习过程。如有错误还请毫不留情的指出来。
与普通的变量对比:
1
2
3graph LR
A[普通变量] --> B[多线程共享] --> C[需要同步控制]
D[ThreadLocal变量] --> E[线程私有] --> F[无锁并发]每个线程访问的变量副本都是独立的,避免了共享变量引起的线程安全问题。由于 Thread Local 实现了变量的线程独占,使得变量不需要同步处理,因此能够避免资源竞争。
二、底层实现原理深度解析
- Thread:每个线程持有
ThreadLocal.ThreadLocalMap
实例 - Thread Local:提供
get()/set()/remove()
等操作方法 - Thread Local Map:自定义哈希表,使用弱引用键(Entry extends Weak Reference)
每个 Thread 实例持有一个由 Thread Local 定义的 Thread Local Map 容器,Thread Local 通过自身实例作为键,在当前线程的 Thread Local Map 中存储 / 获取数据,实现线程间数据隔离。
上图:
下面上一些Thread Local的源码看看
get方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36/**
* 返回当前线程对应的值;若不存在,则调用 setInitialValue 初始化。
*/
public T get() {
Thread t = Thread.currentThread(); //这里的currentThread就是获取当前正在执行代码的线程对象的引用
ThreadLocalMap map = getMap(t); //然后这里拿到这个线程的Thread Local Map
if (map != null) { //判断Map是否存在(初始时线程的threadLocals字段为null,即Map未创建)
ThreadLocalMap.Entry e = map.getEntry(this);//这里的this指向的是Thread Local实例对象,作为key去拿entry
if (e != null) {
T result = (T) e.value;
return result; //拿到结果,return回去
}
}
return setInitialValue();//这里是第一次,get的时候map为null走的逻辑,也就是初始化一下这个线程的map
}
private T setInitialValue() {
T value = initialValue();//null //这里调用的就是下面的initialValue,拿到一个null
Thread t = Thread.currentThread(); //获取当前正在执行代码的线程对象的引用
ThreadLocalMap map = getMap(t); //这里拿到这个线程的Thread Local Map 一开始确实是null哈
if (map != null)
map.set(this, value);// 若ThreadLocalMap已存在,将当前 ThreadLocal实例作为键this,初始值作为值,存入 map
else
createMap(t, value); //创建了一个map
return value;
}
protected T initialValue() {
return null;
}
/**
* 通过 Unsafe 在 Thread 对象上直接写入 threadLocals 字段
*/
private void createMap(Thread t, T firstValue) {
ThreadLocalMap m = new ThreadLocalMap(this, firstValue);
UNSAFE.putObject(t, threadLocalsOffset, m);
}set方法 以Thread Local实例对象为key,value为值,去Thread Local map里面设值
1
2
3
4
5
6
7
8
9
10
11
12/**
* 设置当前线程对应的值
* @param value 要设置的值
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
接下来是Thread Local里面的静态类Thread Local Map的源码分析 🙂
Thread Local Map 中的每个 Entry 以弱引用包装 Thread Local 实例作为键,以强引用存储用户设置的值,弱引用键允许 Thread Local 实例在无强引用时被回收,结合主动清理机制避免内存泄漏。什么是强软弱虚引用后续再说
这张图很形象啊,图来自二哥
Thread → Thread Local Map → Entry(Key=Weak Ref
1 | /* ———— 内部静态类:ThreadLocalMap ———— */ |
三、Thread Local的内存泄露怎么回事?
通过前面的阅读,我们可以知道:Thread Local Map 的 Key是弱引用,但Value是强引用。
Thread Local Map使用Thread Local的弱引用作为key,如果一个Thread Local没有外部强引用引用它,那么系统GC的时候,这个Thread Local势必会被回收,这样一来,Thread local Map中就会出现key为null的Entry,既然key为null,那你就访问不到了,如果当前线程迟迟不能结束(比如你正好用的线程池)这些key为null的Entry的value就会一直存在一条强引用。
怎么解决呢?
使用完 Thread Local 后,及时调用 remove()
方法释放内存空间。
1 | try { |
remove()
方法会将当前线程的 Thread Local Map 中的所有 key 为 null 的 Entry 全部清除,这样就能避免内存泄漏问题。
1 | private void remove(ThreadLocal<?> key) { |
Thread Local Map的扩容机制
- 扩容触发条件:
- 在
ThreadLocalMap
中,当size
(当前有效键值对数量)超过threshold
(阈值)时,会触发扩容。threshold
的初始值为INITIAL_CAPACITY
(默认 16)的 2/3,即threshold = INITIAL_CAPACITY * 2 / 3
。
- 在
- 扩容过程:
- 创建新数组:扩容时,会创建一个新的
Entry
数组,新数组的长度是原来的 2 倍。例如,原来的容量是 16,扩容后变为 32。 - 重新计算索引并迁移数据:遍历旧数组中的每个
Entry
,对于每个非空的Entry
,重新计算其在新数组中的索引位置。计算索引的方式是使用ThreadLocal
实例的threadLocalHashCode
与新数组长度减 1 进行按位与操作(h = k.threadLocalHashCode & (newLen - 1)
)。如果新位置已经被占用,则通过线性探测(while (newTab[h] != null) h = nextIndex(h, newLen);
)找到下一个可用的位置,然后将Entry
放入新数组。 - 更新相关参数:扩容后,
threshold
会更新为新容量的 2/3,table
会指向新的数组。
- 创建新数组:扩容时,会创建一个新的
- 扩容的意义:扩容可以减少哈希冲突,提高
ThreadLocalMap
的查找效率。当哈希表中的元素越来越多,哈希冲突的概率会增加,通过扩容可以重新分配元素的存储位置,使元素分布更加均匀。
四、Thread Local的应用场景
1 | // Web 应用中为每个请求绑定用户上下文 |