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
    3
    graph 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 中存储 / 获取数据,实现线程间数据隔离。
上图:

image-20250511154550617

下面上一些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) {
    @SuppressWarnings("unchecked")
    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, Value=强引用)

image-20250511164618347

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/* ———— 内部静态类:ThreadLocalMap ———— */
/**
* 每个线程持有一个 ThreadLocalMap,通过弱引用存储 ThreadLocal 键
*/
static class ThreadLocalMap { //ThreadLocalMap里面装的是一个个Entry对象,key是弱引用,value是强引用
/**
* map 中的条目:弱引用 key + 强引用 value
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/** 初始容量,必须为 2 的幂 */
private static final int INITIAL_CAPACITY = 16;
/** 存储弱引用 Entry 的数组 */
private Entry[] table;
/** 当前键值对数量 */
private int size = 0;
/** 重哈希阈值:当 size 超过 threshold 时扩容 */
private int threshold;
/**
* 构造:首次插入
* @param firstKey ThreadLocal 实例
* @param firstValue 对应值
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
threshold = INITIAL_CAPACITY * 2 / 3;
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
}
/*----------------------比较重要的一些方法----------------------*/
/**
* 根据 ThreadLocal 实例(键)查找对应的 Entry(开放地址法 + 线性探测)
*/
private Entry getEntry(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 计算初始哈希索引(利用 threadLocalHashCode 保证不同实例哈希值均匀分布)
int i = key.threadLocalHashCode & (len - 1);
Entry e = tab[i];
while (e != null && e.get() != key) {
i = nextIndex(i, len);
e = tab[i];
}
return e;
}

/**
* 插入或更新键值对,同时清理过期弱引用条目并处理扩容。
*aram key ThreadLocal 实例(键)
* @param value 用户存储的值(强引用)
*这里的set方法就是上面的public set方法中最终调用的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;
}
}
// 未找到,插入新 Entry
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

三、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就会一直存在一条强引用。

image-20250511171326742 怎么解决呢?

​ 使用完 Thread Local 后,及时调用 remove() 方法释放内存空间。

1
2
3
4
5
6
try {
threadLocal.set(value);
// 执行业务操作
} finally {
threadLocal.remove(); // 确保能够执行清理
}

remove() 方法会将当前线程的 Thread Local Map 中的所有 key 为 null 的 Entry 全部清除,这样就能避免内存泄漏问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 计算 key 的 hash 值
int i = key.threadLocalHashCode & (len-1);
// 遍历数组,找到 key 为 null 的 Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 将 key 为 null 的 Entry 清除
e.clear();
expungeStaleEntry(i);
return;
}
}
}

public void clear() {
this.referent = null;
}

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
2
3
4
5
6
7
8
9
10
11
// Web 应用中为每个请求绑定用户上下文
private static final ThreadLocal<UserContext> userContext = ThreadLocal.withInitial(UserContext::new);

public void handleRequest(Request req) {
try {
userContext.get().setUserId(req.getParameter("userId"));
// 业务逻辑
} finally {
userContext.remove();
}
}

五、来点面试题