Java 并发 - ThreadLocal
ThreadLocal 是一个存储线程本地副本的工具类。
要保证线程安全,不一定非要进行同步。同步只是保证共享数据争用时的正确性,如果一个方法本来就不涉及共享数据,那么自然无须同步。
Java 中的 无同步方案 有:
- 可重入代码 - 也叫纯代码。如果一个方法,它的 返回结果是可以预测的,即只要输入了相同的数据,就能返回相同的结果,那它就满足可重入性,当然也是线程安全的。
- 线程本地存储 - 使用 ThreadLocal 为共享变量在每个线程中都创建了一个本地副本,这个副本只能被当前线程访问,其他线程无法访问,那么自然是线程安全的。
ThreadLocal 的主要特点是:
- 为每个使用该变量的线程提供独立的变量副本
- 每个线程只能访问和修改自己的副本,不会影响其他线程的副本
- 实现了线程间的数据隔离
使用示例
public class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
Runnable task = () -> {
int value = threadLocal.get();
threadLocal.set(value + 1);
System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
};
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
最佳实践
- 尽量使用
private static final
修饰 ThreadLocal 变量 - 使用完毕后必须调用 remove() 清理
- 考虑使用 InheritableThreadLocal 实现父子线程间的值传递
- 避免存储大对象,防止内存泄漏
实现原理
ThreadLocal 的实现依赖于 Thread 类中的 threadLocals 变量,这是一个 ThreadLocalMap 类型的变量:
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
// ...
}
2
3
4
每个 Thread 对象内部都维护了一个 ThreadLocalMap,ThreadLocalMap 是一个定制化的 HashMap,这个 Map 以 ThreadLocal 实例作为 key,以线程的变量副本作为 value。
主要方法
get():获取当前线程的变量副本
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
1
2
3
4
5
6
7
8
9
10
11
12
13set():设置当前线程的变量副本
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); } else { createMap(t, value); } }
1
2
3
4
5
6
7
8
9remove()
:移除当前线程的变量副本public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
1
2
3
4
5
ThreadLocalMap的特殊设计
ThreadLocal 存储结构的真实 key-value 关系如下:
Thread (线程)
└── ThreadLocalMap (线程私有Map)
├── Entry1: key=ThreadLocal实例1, value=值1
├── Entry2: key=ThreadLocal实例2, value=值2
└── ...
2
3
4
5
关键点:
- 真实key:ThreadLocal对象实例本身(即代码中创建的ThreadLocal变量)
- value:通过 set() 方法设置的值
- 存储位置:每个线程独有的 ThreadLocalMap 中
ThreadLocal 的内部实现类 ThreadLocalMap 有一些特殊设计:
1、弱引用key:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key是弱引用
value = v;
}
}
2
3
4
5
6
7
Entry extends WeakReference<ThreadLocal<?>>
明确使用了弱引用super(k)
调用将key作为弱引用保存- 各种方法中都有对
k == null
的判断处理
设计特点
- Key是弱引用:Entry继承了WeakReference,并将ThreadLocal对象(k)作为弱引用存储。这意味着当没有其他强引用指向ThreadLocal对象时,它会被垃圾回收。
- Value是强引用: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) { // 关键点:如果ThreadLocal已被回收(k为null)
replaceStaleEntry(key, value, i); // 处理key被GC的情况
return;
}
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2、惰性清理机制
ThreadLocalMap 采用三种清理策略组合:
2.1 探测式清理(expungeStaleEntry)
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清理当前staleSlot
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) { // 发现其他失效条目
e.value = null;
tab[i] = null;
size--;
} else {
// rehash逻辑
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
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
特点:
- 线性探测清理连续段
- 同时重组有效条目
- 每次set/get触发局部清理
2.2 启发式清理(cleanSomeSlots)
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
特点:
- 对数复杂度扫描(O(log n))
- 在插入操作时触发
- 平衡清理开销与效果
2.3 全量清理(rehash)
private void rehash() {
expungeStaleEntries();
// 扩容逻辑(阈值2/3)
if (size >= threshold - threshold / 4)
resize();
}
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
特点:
- 扩容前强制执行
- 遍历整个表清理
- 相对耗时的操作
3、哈希算法设计
ThreadLocal 使用特殊的哈希算法:
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);
}
2
3
4
5
6
7
8
9
关键点:
- 斐波那契散列:HASH_INCREMENT = 0x61c88647(黄金分割数相关)
- 原子递增:保证不同ThreadLocal的hashCode分布均匀
- 避免冲突:配合开放寻址法减少聚集现象
4、扩容机制
扩容逻辑在resize()方法中:
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (Entry e : oldTab) {
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // 清理旧值
} else {
// 重新哈希
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
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
特点:
- 容量翻倍
- 扩容时执行全量清理
- 重组所有有效条目
- 新阈值 = 新长度 * 2 / 3
与常规Map的对比
特性 | ThreadLocalMap | HashMap |
---|---|---|
哈希冲突解决 | 开放寻址法(线性探测) | 链表+红黑树 |
键引用类型 | 弱引用 | 强引用 |
清理机制 | 惰性三阶段清理 | 无特殊清理 |
扩容触发条件 | 2/3容量 | 0.75容量 |
哈希算法 | 斐波那契散列 | 扰动函数+取模 |
迭代器 | 不支持 | 支持 |
线程安全 | 单线程访问 | 非线程安全 |
典型使用场景
场景1:数据库连接管理
public class ConnectionManager {
private static ThreadLocal<Connection> connectionHolder =
ThreadLocal.withInitial(() -> {
try {
return DriverManager.getConnection(DB_URL);
} catch (SQLException e) {
throw new RuntimeException(e);
}
});
public static Connection getConnection() {
return connectionHolder.get();
}
public static void setConnection(Connection conn) {
connectionHolder.set(conn);
}
public static void removeConnection() {
connectionHolder.remove();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
每个线程从连接池获取连接后,使用ThreadLocal存储,确保该线程在整个操作过程中使用的是同一个连接,避免了频繁获取和释放连接的开销。
场景2:用户会话信息管理
public class UserContext {
private static ThreadLocal<User> currentUser = new ThreadLocal<>();
public static void setCurrentUser(User user) {
currentUser.set(user);
}
public static User getCurrentUser() {
return currentUser.get();
}
public static void clear() {
currentUser.remove();
}
}
// 在拦截器或过滤器中
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
User user = getUserFromRequest(request);
UserContext.setCurrentUser(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserContext.clear(); // 防止内存泄漏
}
}
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
在Web应用中,使用ThreadLocal存储当前登录用户信息,可以在任何地方通过UserContext获取,避免了在方法参数中层层传递用户信息。
场景3:日期格式化
public class DateUtils {
// SimpleDateFormat不是线程安全的
private static ThreadLocal<SimpleDateFormat> dateFormatHolder =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static String format(Date date) {
return dateFormatHolder.get().format(date);
}
}
2
3
4
5
6
7
8
9
SimpleDateFormat不是线程安全的,使用ThreadLocal为每个线程提供独立的实例,既保证了线程安全又避免了频繁创建对象的开销。
场景4:分页参数管理
public class PageContext {
private static ThreadLocal<PageInfo> pageInfoHolder = new ThreadLocal<>();
public static void setPageInfo(int pageNum, int pageSize) {
pageInfoHolder.set(new PageInfo(pageNum, pageSize));
}
public static PageInfo getPageInfo() {
return pageInfoHolder.get();
}
public static void clear() {
pageInfoHolder.remove();
}
}
// 在Controller中
@GetMapping("/users")
public List<User> getUsers(@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
PageContext.setPageInfo(page, size);
return userService.getUsers();
}
// 在Service中
public List<User> getUsers() {
PageInfo pageInfo = PageContext.getPageInfo();
// 使用分页参数查询
}
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
将分页参数存储在ThreadLocal中,避免了在方法调用链中层层传递分页参数。
内存泄漏问题
ThreadLocal 可能引起内存泄漏,原因在于:
- ThreadLocalMap 的 key 是弱引用(WeakReference),但 value 是强引用
- 当 ThreadLocal 外部强引用被回收后,key 会被 GC 回收,但 value 仍然存在
- 如果线程长时间运行(如线程池中的线程),会导致 value 无法被回收
内存泄漏场景示例
内存泄漏场景:
ExecutorService pool = Executors.newFixedThreadPool(1);
ThreadLocal<byte[]> tl = new ThreadLocal<>();
pool.execute(() -> {
tl.set(new byte[1024 * 1024]); // 1MB
// 忘记调用tl.remove()
});
tl = null; // 强引用解除
// 线程池线程存活,value永远无法回收
2
3
4
5
6
7
8
9
10
展示 ThreadLocal 的内存管理机制和潜在的内存泄漏问题:
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
public class ThreadLocalWeakRefTest {
public static void main(String[] args) throws InterruptedException {
// 1. 测试ThreadLocal的Entry key(弱引用)被回收情况
testThreadLocalWeakRef();
// 2. 对比普通WeakReference的行为
testStandardWeakRef();
}
private static void testThreadLocalWeakRef() throws InterruptedException {
System.out.println("===== ThreadLocal弱引用测试 =====");
ThreadLocal<Object> tl = new ThreadLocal<>();
Object value = new Object();
// 保存value的弱引用用于后续验证
WeakReference<Object> valueWeakRef = new WeakReference<>(value);
tl.set(value);
// 第一次GC - tl强引用仍然存在
System.gc();
Thread.sleep(500); // 给GC一点时间
System.out.println("[tl强引用存在时]");
System.out.println("ThreadLocal value: " + (tl.get() != null)); // true
System.out.println("value对象存活: " + (valueWeakRef.get() != null)); // true
// 解除ThreadLocal的强引用
tl = null;
// 第二次GC - tl只有Entry中的弱引用
System.gc();
Thread.sleep(500);
// 获取当前线程的ThreadLocalMap查看Entry状态
Thread currentThread = Thread.currentThread();
java.lang.reflect.Field threadLocalsField;
try {
threadLocalsField = Thread.class.getDeclaredField("threadLocals");
threadLocalsField.setAccessible(true);
Object threadLocalMap = threadLocalsField.get(currentThread);
if (threadLocalMap != null) {
java.lang.reflect.Field tableField = threadLocalMap.getClass().getDeclaredField("table");
tableField.setAccessible(true);
Object[] table = (Object[]) tableField.get(threadLocalMap);
System.out.println("\n[解除tl强引用后]");
System.out.println("ThreadLocalMap中Entry数量: " + table.length);
boolean foundStaleEntry = false;
for (Object entry : table) {
if (entry != null) {
// 获取Entry的referent(即key/ThreadLocal对象)
Method getMethod = Reference.class.getDeclaredMethod("get");
getMethod.setAccessible(true);
Object referent = getMethod.invoke(entry);
// 获取Entry的value
java.lang.reflect.Field valueField = entry.getClass().getDeclaredField("value");
valueField.setAccessible(true);
Object entryValue = valueField.get(entry);
if (referent == null) {
foundStaleEntry = true;
System.out.println("发现key被回收的Entry, value: " + entryValue);
System.out.println("value对象存活: " + (valueWeakRef.get() != null)); // true - 内存泄漏!
}
}
}
if (!foundStaleEntry) {
System.out.println("未找到key被回收的Entry");
}
}
} catch (Exception e) {
e.printStackTrace();
}
// 3. 调用remove清理
// 由于我们已失去tl引用,无法直接调用remove
// 这模拟了实际开发中忘记调用remove的情况
System.out.println("\n由于失去ThreadLocal引用,无法调用remove()");
System.out.println("value对象仍然存活: " + (valueWeakRef.get() != null));
// 4. 模拟ThreadLocal的自我清理机制
System.out.println("\n模拟ThreadLocal的set/get触发清理...");
// 新建一个ThreadLocal触发清理
ThreadLocal<Object> newTl = new ThreadLocal<>();
newTl.set(new Object()); // 这会触发清理逻辑
newTl.get(); // 也会触发清理
// 再次检查value
System.out.println("value对象存活: " + (valueWeakRef.get() != null)); // 可能已被清理
}
private static void testStandardWeakRef() throws InterruptedException {
System.out.println("\n===== 标准WeakReference测试 =====");
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);
System.out.println("强引用存在时: " + (weakRef.get() != null)); // true
// 第一次GC - 强引用仍然存在
System.gc();
Thread.sleep(500);
System.out.println("强引用存在时GC后: " + (weakRef.get() != null)); // true
// 解除强引用
obj = null;
// 第二次GC - 只有弱引用
System.gc();
Thread.sleep(500);
System.out.println("解除强引用后GC: " + (weakRef.get() != null)); // false
}
}
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
关键点说明:
- ThreadLocal内存泄漏演示:
- 当ThreadLocal强引用被置为null后,Entry中的key(弱引用)会被回收
- 但value仍然保持强引用,导致内存泄漏
- 通过反射查看ThreadLocalMap内部状态验证这一点
- 与标准WeakReference对比:
- 标准WeakReference在强引用消失后,对象会被回收
- ThreadLocal的Entry设计特殊,只有key是弱引用,value不是
- ThreadLocal的自我清理:
- 当调用ThreadLocal的set/get时,会清理key为null的Entry
- 示例中通过新建ThreadLocal并操作来触发这个机制
运行结果:
===== ThreadLocal弱引用测试 =====
[tl强引用存在时]
ThreadLocal value: true
value对象存活: true
[解除tl强引用后]
ThreadLocalMap中Entry数量: 16
发现key被回收的Entry, value: java.lang.Object@330bedb4
value对象存活: true
由于失去ThreadLocal引用,无法调用remove()
value对象仍然存活: true
模拟ThreadLocal的set/get触发清理...
value对象存活: true
===== 标准WeakReference测试 =====
强引用存在时: true
强引用存在时GC后: true
解除强引用后GC: false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- ThreadLocal弱引用测试
[tl强引用存在时]
阶段:ThreadLocal value: true
(正常)value对象存活: true
(正常)
[解除tl强引用后]
阶段:- 发现了key被回收的Entry(
java.lang.Object@330bedb4
) - 但
value对象存活: true
→ 确认value未被回收 - 这正是ThreadLocal内存泄漏的典型表现
- 发现了key被回收的Entry(
- 模拟set/get清理后value仍然存活
- 标准WeakReference测试,完全符合预期
- 强引用存在时对象存活
- 解除强引用后GC,对象被回收
问题分析
- 为什么ThreadLocal的value没有被自动清理?
- ThreadLocalMap的清理是"惰性清理",只有以下操作会触发:
- 调用set()时遇到key为null的Entry
- 调用get()时遇到key为null的Entry
- 调用remove()时
- 测试中虽然模拟了set/get,但可能:
- 没有触发再哈希过程
- 没有扫描到那个特定的Entry
- ThreadLocalMap的清理是"惰性清理",只有以下操作会触发:
- 内存泄漏的严重程度:
- 如果线程是线程池的工作线程(长期存活)
- 泄漏的value会一直存在,直到:
- 线程死亡
- 恰好有set/get操作触发了清理
- 手动调用remove()
解决方案
1、必须遵循的使用规范:
try {
// 使用ThreadLocal
threadLocal.set(someValue);
// ...
} finally {
threadLocal.remove(); // 必须手动清理
}
2
3
4
5
6
7
查看remove()
方法的实现:
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.get() == key) {
e.clear(); // 清除WeakReference
expungeStaleEntry(i); // 清除整个Entry
return;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
其中expungeStaleEntry()
会清理key为null的Entry,释放value的内存。
2、对于线程池场景的特别处理:
ExecutorService pool = Executors.newCachedThreadPool();
pool.execute(() -> {
try {
threadLocal.set(value);
// ...任务代码...
} finally {
threadLocal.remove(); // 必须清理
}
});
2
3
4
5
6
7
8
9