ThreadLocal(本地线程变量)
# ThreadLocal(本地线程变量)
前言
在并发编程领域,确保线程安全是核心挑战之一。ThreadLocal
作为 Java 并发包 (java.lang
) 提供的独特机制,为解决多线程环境下共享变量的访问冲突提供了一种巧妙的思路。它并非通过加锁等同步手段来保证共享,而是为每个使用该变量的线程创建一个独立的副本,使得每个线程都能独立地修改自己的副本,而不会影响其他线程,从而天然地避免了并发问题。本文将深入探讨 ThreadLocal
的核心概念、工作原理、典型应用场景,并着重分析其潜在的内存泄漏风险及规避策略,同时介绍其扩展 InheritableThreadLocal
和在线程池场景下的利器 TransmittableThreadLocal
。
# 1. ThreadLocal 概述与核心思想
# 1.1 什么是 ThreadLocal?
ThreadLocal
,顾名思义,即“线程本地变量”。它提供了一种变量作用域的扩展,超越了传统的方法作用域、实例作用域和类作用域,创建了一种“线程作用域”。当你创建一个 ThreadLocal
变量时,访问这个变量的每个线程都会拥有该变量的一个本地、独立的初始值副本。线程对这个副本的任何修改都只影响当前线程,对其他线程是不可见的。
这种机制的本质是数据隔离。它将共享数据的并发访问问题,转化为每个线程访问自己私有数据的模式,从而避免了同步。
# 1.2 背景:多线程下的共享变量问题
在多线程环境中,多个线程可能会同时读写同一个共享变量。这些共享变量通常存储在:
- 堆内存 (Heap):实例对象的成员变量、静态变量、数组元素等。
Java 内存模型 (JMM) 定义了线程如何与主内存以及它们各自的工作内存交互。当多个线程操作共享变量时,若没有适当的同步措施,就可能出现数据竞争、可见性问题、指令重排序等导致的线程安全问题。
# 1.3 ThreadLocal vs. 同步锁 (Synchronized / Lock)
ThreadLocal
和传统的同步锁机制(如 synchronized
关键字或 java.util.concurrent.locks.Lock
接口)是解决线程安全问题的两种不同哲学:
特性 | 同步锁 (Synchronized / Lock) | ThreadLocal |
---|---|---|
核心思想 | 时间换空间:只保留一份共享变量,通过锁机制让线程排队访问,牺牲并发性来保证数据一致性。 | 空间换时间:为每个线程创建变量副本,用额外的内存空间换取线程间的无锁并发访问。 |
关注点 | 保证多个线程对共享资源访问的同步性和互斥性。 | 实现多个线程对状态的隔离性,避免共享。 |
适用场景 | 多个线程需要协作完成对同一份数据的修改或访问。 | 每个线程需要维护自身独立的状态或上下文信息。 |
ThreadLocal
并不解决多个线程需要访问同一个共享实例的问题,而是为每个线程提供了一个独立的、隔离的存储空间。
# 2. ThreadLocal 的核心 API 与工作原理
# 2.1 基本使用示例
ThreadLocal
的使用通常遵循以下模式:
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 模拟用户实体类
*/
class User {
private String name;
// ... 其他属性和方法 ...
public User(String name) { this.name = name; }
@Override public String toString() { return "User{name='" + name + "'}"; }
}
/**
* ThreadLocal 基本使用演示
*/
public class ThreadLocalBasicDemo {
// 1. 声明 ThreadLocal 变量 (通常用 private static final 修饰)
// 泛型 <User> 指定了 ThreadLocal 存储的数据类型
private static final ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
// 也可以使用 withInitial 提供初始值工厂
private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> {
System.out.println("Initializing SimpleDateFormat for thread: " + Thread.currentThread().getName());
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
});
/**
* 向当前线程的 ThreadLocal 中存储数据
* @param user 要存储的用户对象
*/
public void setUserForCurrentThread(User user) {
System.out.println(Thread.currentThread().getName() + " setting user: " + user);
// 调用 set 方法,将 user 对象与当前线程关联起来
threadLocalUser.set(user);
}
/**
* 从当前线程的 ThreadLocal 中获取数据
* @return 当前线程关联的用户对象,如果未设置过则返回 null (除非使用 withInitial)
*/
public User getUserForCurrentThread() {
// 调用 get 方法,获取与当前线程关联的 User 对象
User user = threadLocalUser.get();
System.out.println(Thread.currentThread().getName() + " getting user: " + user);
return user;
}
/**
* 清除当前线程在 ThreadLocal 中存储的数据
* !!! 非常重要:防止内存泄漏,尤其在线程池场景下必须调用 !!!
*/
public void clearUserForCurrentThread() {
System.out.println(Thread.currentThread().getName() + " removing user...");
// 调用 remove 方法,断开当前线程与存储值的关联
threadLocalUser.remove();
}
/**
* 使用带初始值的 ThreadLocal
*/
public String getCurrentFormattedDate() {
// 第一次调用 get 时,如果当前线程没有值,会调用 withInitial 提供的 Supplier 创建初始值
SimpleDateFormat sdf = dateFormatThreadLocal.get();
return sdf.format(new Date());
}
public static void main(String[] args) {
ThreadLocalBasicDemo demo = new ThreadLocalBasicDemo();
// 创建两个线程来演示线程隔离性
Thread thread1 = new Thread(() -> {
demo.setUserForCurrentThread(new User("Alice")); // Thread-0 设置 User
demo.getUserForCurrentThread(); // Thread-0 获取自己的 User
System.out.println("Thread-0 Date: " + demo.getCurrentFormattedDate()); // Thread-0 获取日期格式化器
demo.clearUserForCurrentThread(); // Thread-0 清理
demo.getUserForCurrentThread(); // 清理后再获取为 null
}, "Thread-0");
Thread thread2 = new Thread(() -> {
try { Thread.sleep(50); } catch (InterruptedException e) {} // 确保 Thread-0 先执行一部分
demo.setUserForCurrentThread(new User("Bob")); // Thread-1 设置自己的 User
demo.getUserForCurrentThread(); // Thread-1 获取自己的 User,与 Thread-0 不同
System.out.println("Thread-1 Date: " + demo.getCurrentFormattedDate()); // Thread-1 获取自己的日期格式化器
demo.clearUserForCurrentThread(); // Thread-1 清理
}, "Thread-1");
thread1.start();
thread2.start();
}
}
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
# 2.2 ThreadLocal 核心 API 详解
ThreadLocal
类本身非常简单,主要提供以下三个公共方法:
public void set(T value)
:- 作用: 将指定的值
value
与当前正在执行的线程关联起来。这个值存储在当前线程内部的一个特殊 Map 中。 - 行为: 如果当前线程之前没有为此
ThreadLocal
对象设置过值,则创建关联;如果设置过,则覆盖旧值。
- 作用: 将指定的值
public T get()
:- 作用: 获取当前正在执行的线程与此
ThreadLocal
对象关联的值。 - 行为:
- 如果当前线程之前调用过
set()
设置了值,则返回该值。 - 如果当前线程从未调用过
set()
,并且该ThreadLocal
是通过ThreadLocal.withInitial(Supplier)
创建的,则会调用Supplier
的get()
方法生成一个初始值,将该初始值与当前线程关联,并返回。 - 如果当前线程从未调用过
set()
,且没有提供withInitial
,则返回null
。
- 如果当前线程之前调用过
- 作用: 获取当前正在执行的线程与此
public void remove()
:- 作用: 移除当前正在执行的线程与此
ThreadLocal
对象的关联。 - 行为: 清除当前线程内部 Map 中关于此
ThreadLocal
的条目。这是防止内存泄漏的关键操作。调用后,如果再次调用get()
,其行为将如同从未调用过set()
一样(可能返回初始值或null
)。
- 作用: 移除当前正在执行的线程与此
关键点:
- 一个
ThreadLocal
实例对应线程内部存储结构中的一个条目 (Entry)。如果你需要为每个线程存储多个不同类型或不同用途的数据,你需要创建多个ThreadLocal
实例。remove()
方法至关重要,尤其是在使用线程池时。线程池中的线程会被复用,如果不显式remove()
,上一个任务设置的ThreadLocal
值可能会被下一个使用该线程的任务误读,并且导致内存泄漏。
# 2.3 ThreadLocal 工作原理解析
ThreadLocal
的魔法并不在 ThreadLocal
类本身,而是在 java.lang.Thread
类以及 ThreadLocal
的一个静态内部类 ThreadLocalMap
中。
核心机制: 每个 Thread
对象内部都有一个成员变量 threadLocals
(以及另一个用于继承的 inheritableThreadLocals
),这个变量的类型是 ThreadLocal.ThreadLocalMap
。
// Thread 类内部结构 (简化示意)
public class Thread implements Runnable {
// ... 其他成员变量 ...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null; // 存储普通 ThreadLocal 的 Map
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 存储可继承 ThreadLocal 的 Map
// ... 构造函数、方法等 ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ThreadLocalMap
:
- 它是一个定制化的哈希映射 (Map),专门用于存储线程本地变量。
- 它的 Key 是
ThreadLocal
对象本身 (更准确地说,是ThreadLocal
对象的弱引用WeakReference<ThreadLocal<?>>
)。 - 它的 Value 就是通过
ThreadLocal.set(value)
设置的那个value
对象。 ThreadLocalMap
的实例是存储在Thread
对象内部的,而不是ThreadLocal
对象内部。
# 2.3.1 set(T value)
方法原理
public void set(T value) {
// 1. 获取当前正在执行的线程
Thread t = Thread.currentThread();
// 2. 获取当前线程内部的 threadLocals 这个 Map
ThreadLocalMap map = getMap(t); // getMap(t) 实际上就是返回 t.threadLocals
// 3. 判断 Map 是否已经存在
if (map != null) {
// 3.1 Map 已存在,直接将 <this ThreadLocal 对象, value> 存入 Map
// 这里的 'this' 就是调用 set 方法的那个 ThreadLocal 实例
map.set(this, value);
} else {
// 3.2 Map 不存在 (第一次为该线程设置 ThreadLocal 值)
// 创建一个新的 ThreadLocalMap,并将 <this ThreadLocal 对象, value> 作为第一个条目放入
createMap(t, value); // createMap(t, value) 内部会执行 t.threadLocals = new ThreadLocalMap(this, value);
}
}
// ThreadLocalMap.set(key, value) 内部大致逻辑:
// - 计算 key (ThreadLocal 对象) 的哈希码
// - 根据哈希码找到或计算在内部 Entry[] 数组中的索引
// - 如果该位置为空,则创建新 Entry(key, value) 放入
// - 如果该位置已有 Entry:
// - 若 Key 相同,则覆盖旧 Value
// - 若 Key 不同 (哈希冲突),则使用线性探测法查找下一个可用位置
// - 在探测过程中,会清理 Key 为 null (弱引用已被回收) 的 "脏" Entry (expungeStaleEntry)
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
# 2.3.2 get()
方法原理
public T get() {
// 1. 获取当前线程
Thread t = Thread.currentThread();
// 2. 获取当前线程的 threadLocals Map
ThreadLocalMap map = getMap(t);
// 3. 判断 Map 是否存在
if (map != null) {
// 3.1 Map 存在,尝试根据 'this' (当前 ThreadLocal 对象) 作为 Key 获取对应的 Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 3.1.1 Entry 存在,直接返回 Entry 中存储的 Value
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 4. Map 不存在或 Map 中没有找到对应的 Entry
// 调用 setInitialValue() 来获取/设置初始值
return setInitialValue();
}
private T setInitialValue() {
// 4.1 调用 initialValue() 方法获取初始值 (默认返回 null,可通过 withInitial 自定义)
T value = initialValue(); // protected T initialValue() { return null; }
// 4.2 获取当前线程和 Map (如果 Map 不存在则创建)
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 4.3 将初始值存入 Map
map.set(this, value);
} else {
createMap(t, value);
}
// 4.4 返回初始值
return value;
}
// ThreadLocalMap.getEntry(key) 内部大致逻辑:
// - 计算 key 的哈希码,找到初始索引
// - 从该索引开始线性探测 Entry[] 数组:
// - 如果找到 Entry 且其 Key (弱引用指向的对象) 与传入的 key 相同,返回该 Entry
// - 如果找到 Entry 但其 Key 为 null (已被回收),调用 expungeStaleEntry 清理,然后继续探测
// - 如果探测到 null (数组空位),说明 Key 不存在,返回 null
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
# 2.3.3 remove()
方法原理
public void remove() {
// 1. 获取当前线程的 threadLocals Map
ThreadLocalMap m = getMap(Thread.currentThread());
// 2. 如果 Map 存在
if (m != null) {
// 2.1 调用 Map 的 remove 方法,传入 'this' (当前 ThreadLocal 对象) 作为 Key
m.remove(this);
}
}
// ThreadLocalMap.remove(key) 内部大致逻辑:
// - 计算 key 的哈希码,找到初始索引
// - 从该索引开始线性探测:
// - 如果找到 Entry 且其 Key 与传入的 key 相同:
// - 将该 Entry 的 Key (弱引用) clear 掉
// - 调用 expungeStaleEntry 清理该位置及其后续可能存在的脏条目
// - 返回
// - 如果探测到 null,说明 Key 不存在,直接返回
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2.4 ThreadLocalMap
内部结构简述
Entry
数组:ThreadLocalMap
内部维护一个Entry[] table
数组,用于存储键值对。数组大小总是 2 的幂。Entry
类:static class Entry extends WeakReference<ThreadLocal<?>>
。- 继承自
WeakReference
,其引用的对象是ThreadLocal
实例 (Key)。这意味着,即使ThreadLocal
对象在外部没有强引用了,GC 也可以回收它。 - 包含一个
Object value
成员,存储线程本地变量的值。
- 继承自
- 哈希与冲突解决: 使用
threadLocalHashCode
(与ThreadLocal
对象关联的一个固定哈希值) 计算索引i = key.threadLocalHashCode & (table.length - 1)
。使用线性探测法 (Linear Probing) 解决哈希冲突。 - 扩容: 当 Map 中元素数量超过阈值 (数组长度的 2/3) 时,会进行扩容 (rehash),通常是将数组大小翻倍。
- 过期条目清理 (
expungeStaleEntry
): 在set
,get
,remove
操作中,当遇到 Key 为null
的 Entry (即ThreadLocal
对象已被 GC 回收) 时,会触发清理逻辑,将该 Entry 的value
设为null
,并可能重新整理 (rehash) 后续的冲突条目,以帮助释放value
对象占用的内存。
# 2.5 设计演进:为什么是 Thread
持有 Map
?
早期的 ThreadLocal
设计可能是每个 ThreadLocal
对象持有一个 Map<Thread, T>
。但现在的设计 (Thread
持有 Map<ThreadLocal, T>
) 更优:
- 生命周期绑定: 当
Thread
结束时,其拥有的ThreadLocalMap
自然也结束生命周期,有助于内存回收。 - 减少 Map 数量: 通常
ThreadLocal
实例的数量远少于活跃线程的数量,每个线程持有一个 Map 比每个ThreadLocal
持有一个 Map 更节省空间。 - 分散存储: 数据分散存储在各个线程内部,访问时通常只需要访问当前线程的 Map,减少了潜在的锁竞争(虽然
ThreadLocalMap
本身的操作不是线程安全的,但它只被持有它的那个线程访问)。
# 3. ThreadLocal 内存泄漏问题深度剖析
虽然 ThreadLocal
提供了方便的线程数据隔离,但如果使用不当,尤其是在线程池场景下,可能会引发内存泄漏。
# 3.1 内存泄漏的原因
内存泄漏的核心在于 ThreadLocalMap
中 Entry
的设计以及 ThreadLocalMap
的生命周期:
ThreadLocalMap
的生命周期与Thread
绑定: 只要线程存活,ThreadLocalMap
就存活。Entry
的 Key 是弱引用:Entry(ThreadLocal<?> k, Object v)
中,k
是对ThreadLocal
对象的弱引用 (WeakReference
)。这意味着,当外部不再有强引用指向ThreadLocal
对象时,GC 在下一次执行时会回收ThreadLocal
对象本身,并将Entry
中的弱引用k
置为null
。Entry
的 Value 是强引用:Entry
中的value
成员是对实际存储的线程本地变量值的强引用。
泄漏路径:
- 当一个
ThreadLocal
对象不再被任何强引用指向时,GC 会回收它。 - 此时,
ThreadLocalMap
中对应的Entry
的 Key (弱引用) 变为null
。 - 但是,如果持有该
ThreadLocalMap
的线程仍然存活(比如线程池中的核心线程),并且代码中没有显式调用该ThreadLocal
对象的remove()
方法,那么这个 Key 为null
的Entry
仍然存在于ThreadLocalMap
的table
数组中。 - 这个
Entry
对象本身不会被回收(因为它被table
数组引用),更重要的是,它所持有的value
对象(通过强引用)也不会被回收! - 如果这个
value
对象很大,或者不断有新的不再使用的ThreadLocal
变量积累在线程的Map
中,就会导致内存泄漏,最终可能引发OutOfMemoryError
。
图示:
(上图展示了 ThreadLocal 对象被回收后,Value 仍被 Entry 强引用的情况)
# 3.2 为什么设计成弱引用 Key?
既然弱引用 Key 不能完全避免内存泄漏,为什么还要这样设计?
弱引用提供了一种补救机制。ThreadLocalMap
在执行 set()
, get()
, remove()
操作时,会检查遇到的 Entry
的 Key 是否为 null
。如果为 null
,说明对应的 ThreadLocal
对象已被回收,此时 Map
会执行 expungeStaleEntry
操作,尝试将这个 Entry
的 value
设置为 null
,并清理掉这个 "脏" Entry,从而使得 value
对象可以被 GC 回收。
但是,这种清理是被动的,它只在调用 set/get/remove
时顺带发生,并且不保证清理所有过期的 Entry。如果一个线程设置了 ThreadLocal
值后,长时间不再对该 ThreadLocal
或该 Map 进行任何操作,那么即使 ThreadLocal
对象被回收了,对应的 value
仍可能一直泄漏。
对比强引用 Key:
如果 Key 是强引用,那么只要线程存活,即使 ThreadLocal
对象在外部没有引用了,ThreadLocalMap
中的 Entry
对 ThreadLocal
对象的强引用也会阻止 ThreadLocal
对象被回收,进而 value
也无法回收,内存泄漏会更严重。
# 3.3 防止内存泄漏的最佳实践:remove()
最根本、最可靠的防止 ThreadLocal
内存泄漏的方法是:在使用完 ThreadLocal
变量后,务必显式调用其 remove()
方法。
remove()
方法会直接将当前线程 ThreadLocalMap
中对应于该 ThreadLocal
实例的 Entry
清理掉(包括将 Key 和 Value 都置为 null
),从而彻底断开 value
对象的强引用链,使其可以被 GC 正常回收。
尤其是在使用线程池的场景下,remove()
几乎是强制性的。 因为线程池会复用线程,如果不清理,线程执行完一个任务后,其 ThreadLocalMap
中存储的数据会遗留到下一个任务,不仅可能导致数据错乱,还会造成内存泄漏。
推荐使用 try-finally
结构确保 remove()
被执行:
// 获取或初始化 ThreadLocal 实例
private static final ThreadLocal<SomeResource> resourceThreadLocal = ThreadLocal.withInitial(SomeResource::new);
public void processRequest() {
// 从 ThreadLocal 获取资源 (如果不存在则初始化)
SomeResource resource = resourceThreadLocal.get();
try {
// --- 核心业务逻辑 ---
// 使用 resource 对象...
resource.doSomething();
} finally {
// --- 清理工作 ---
// 无论业务逻辑是否成功或抛出异常,都确保移除 ThreadLocal 值
resourceThreadLocal.remove();
System.out.println("ThreadLocal resource removed for thread: " + Thread.currentThread().getName());
}
}
// 模拟资源类
static class SomeResource {
public SomeResource() { System.out.println("Creating SomeResource..."); }
public void doSomething() { System.out.println("Doing something with resource..."); }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 4. ThreadLocal 的典型应用场景
ThreadLocal
的核心价值在于提供线程隔离的数据存储,非常适合以下场景:
# 4.1 场景一:管理每个请求的上下文信息(隐式参数传递)
在 Web 应用或分布式服务中,一个请求的处理可能跨越多个方法调用或多个服务层次(Controller -> Service -> DAO)。如果每个方法都需要获取当前请求的用户信息、追踪 ID (Trace ID)、租户 ID 等上下文信息,通过方法参数层层传递会非常繁琐且增加代码耦合。
ThreadLocal
可以完美解决这个问题:在请求开始时(如 Filter 或 Interceptor 中),将上下文信息存入 ThreadLocal
;在调用链的任何地方,都可以方便地从中获取;在请求结束时(如 Filter 或 Interceptor 的 afterCompletion
),务必清理 ThreadLocal
。
/**
* 用户上下文持有器
*/
public class UserContextHolder {
// 使用 ThreadLocal 存储当前线程的用户信息
private static final ThreadLocal<User> userContext = new ThreadLocal<>();
// 设置当前线程的用户
public static void setUser(User user) {
if (user != null) {
userContext.set(user);
} else {
userContext.remove(); // 如果传入 null,则认为是清除操作
}
}
// 获取当前线程的用户
public static User getUser() {
return userContext.get();
}
// 清除当前线程的用户信息 (推荐使用此方法显式清除)
public static void clear() {
userContext.remove();
}
}
// --- 在 Web Filter 或 Interceptor 中使用 ---
/*
public class ContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1. 从请求中解析用户信息 (如 JWT Token, Session 等)
User user = parseUserFromRequest(request);
// 2. 将用户信息存入 ThreadLocal
UserContextHolder.setUser(user);
return true; // 继续处理请求
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 3. 请求处理完毕后,无论成功失败,务必清理 ThreadLocal
UserContextHolder.clear();
}
// ...
}
*/
// --- 在 Service 层或任何需要的地方获取 ---
/*
public class OrderService {
public void createOrder(Order order) {
// 直接从 ThreadLocal 获取当前用户信息,无需方法参数传递
User currentUser = UserContextHolder.getUser();
if (currentUser != null) {
order.setCreatorId(currentUser.getId());
// ... 其他业务逻辑 ...
System.out.println("User " + currentUser.getName() + " is creating order...");
} else {
// 处理未认证用户的情况
}
}
}
*/
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
实际案例: Spring 框架的事务管理 (@Transactional
) 就是一个经典例子。Spring 使用 ThreadLocal
来存储每个事务线程对应的数据库连接 (Connection
),确保同一个事务内所有数据库操作都使用同一个连接,并在事务结束时关闭连接并清理 ThreadLocal
。
# 4.2 场景二:存储用户身份认证信息
类似于上下文传递,可以在用户登录认证成功后,将用户的身份凭证(如用户 ID、角色列表等解析后的信息,注意不要存储敏感的原始密码或 Token)存入 ThreadLocal
,方便后续业务逻辑进行权限判断或记录操作日志。
import java.util.Map;
import java.util.HashMap;
/**
* 存储当前线程认证信息的持有器
*/
public class AuthInfoHolder {
// 使用 ThreadLocal 存储 Map,可以存放多项认证信息
private static final ThreadLocal<Map<String, Object>> authInfoContext = ThreadLocal.withInitial(HashMap::new);
/**
* 存储认证信息键值对
* @param key 信息键,如 "userId", "roles", "tenantId"
* @param value 信息值
*/
public static void set(String key, Object value) {
authInfoContext.get().put(key, value);
}
/**
* 批量设置认证信息
* @param infoMap 包含认证信息的 Map
*/
public static void setAll(Map<String, Object> infoMap) {
if (infoMap != null) {
// 创建一个新的 Map 副本存入,避免外部修改影响 ThreadLocal 中的值
authInfoContext.set(new HashMap<>(infoMap));
} else {
authInfoContext.remove();
}
}
/**
* 根据键获取认证信息
* @param key 信息键
* @return 对应的值,如果不存在则返回 null
*/
@SuppressWarnings("unchecked")
public static <T> T get(String key) {
return (T) authInfoContext.get().get(key);
}
/**
* 获取当前用户的 ID
* @return 用户 ID,可能为 null
*/
public static Long getUserId() {
return get("userId");
}
/**
* 获取所有认证信息
* @return 包含所有认证信息的 Map 副本
*/
public static Map<String, Object> getAll() {
// 返回副本,防止外部修改
return new HashMap<>(authInfoContext.get());
}
/**
* 清除当前线程的所有认证信息
*/
public static void clear() {
authInfoContext.remove();
}
}
// --- 在认证 Filter/Interceptor 中使用 ---
/*
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 1. 验证 Token 或 Session ...
if (authenticationSuccessful) {
// 2. 解析认证信息
Map<String, Object> authInfo = parseAuthInfo(credentials);
// 3. 存入 ThreadLocal
AuthInfoHolder.setAll(authInfo);
return true;
}
// ... 认证失败处理 ...
return false;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 4. 清理 ThreadLocal
AuthInfoHolder.clear();
}
*/
// --- 在需要权限校验或记录日志的地方使用 ---
/*
public void sensitiveOperation() {
Long currentUserId = AuthInfoHolder.getUserId();
List<String> roles = AuthInfoHolder.get("roles");
if (roles != null && roles.contains("ADMIN")) {
// ... 执行管理员操作 ...
log.info("Admin operation performed by user: {}", currentUserId);
} else {
throw new AccessDeniedException("Permission denied for user: " + currentUserId);
}
}
*/
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
# 4.3 场景三:解决非线程安全的类在并发环境下的使用问题
有些类(如 java.text.SimpleDateFormat
)本身不是线程安全的,如果在多线程环境中共享同一个实例,可能会导致解析或格式化结果出错。
ThreadLocal
可以为此类场景提供解决方案:为每个线程创建一个独立的实例,存放在 ThreadLocal
中。这样每个线程都使用自己的实例,避免了线程安全问题,同时也避免了每次使用时都创建新对象的开销。
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 线程安全的日期格式化工具类
*/
public class SafeDateFormatter {
// 使用 ThreadLocal 为每个线程提供独立的 SimpleDateFormat 实例
// 通过 withInitial 设置初始值,当线程第一次调用 get() 时会自动创建
private static final ThreadLocal<SimpleDateFormat> sdfThreadLocal =
ThreadLocal.withInitial(() -> {
System.out.println("Creating SimpleDateFormat for thread: " + Thread.currentThread().getName());
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
});
/**
* 将 Date 对象格式化为字符串
* @param date 要格式化的日期
* @return 格式化后的字符串
*/
public static String format(Date date) {
// 从 ThreadLocal 获取当前线程对应的 SimpleDateFormat 实例
return sdfThreadLocal.get().format(date);
}
/**
* 将字符串解析为 Date 对象
* @param dateString 要解析的日期字符串
* @return 解析后的 Date 对象
* @throws ParseException 如果解析失败
*/
public static Date parse(String dateString) throws ParseException {
// 从 ThreadLocal 获取当前线程对应的 SimpleDateFormat 实例
return sdfThreadLocal.get().parse(dateString);
}
// 无需 remove() 方法,因为 SimpleDateFormat 对象通常希望与线程生命周期一致
// 除非有特殊需要(如切换格式),一般不移除
public static void main(String[] args) {
// 模拟多线程并发使用
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Date now = new Date();
String formatted = SafeDateFormatter.format(now);
System.out.println(Thread.currentThread().getName() + " formatted: " + formatted);
try {
Date parsed = SafeDateFormatter.parse(formatted);
System.out.println(Thread.currentThread().getName() + " parsed: " + parsed);
} catch (ParseException e) {
e.printStackTrace();
}
}, "Thread-" + i).start();
}
}
}
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
注意: 对于 SimpleDateFormat
这类场景,Java 8 引入的 java.time.format.DateTimeFormatter
本身就是线程安全的,是更好的选择。此示例主要用于演示 ThreadLocal
如何解决非线程安全对象的并发使用问题。
# 5. InheritableThreadLocal:让子线程继承父线程的值
标准的 ThreadLocal
变量的值在父子线程之间是不能传递的。即,如果父线程设置了一个 ThreadLocal
值,然后创建了一个子线程,子线程通过 get()
方法获取到的将是 null
(或初始值),而不是父线程设置的值。
为了解决这个问题,Java 提供了 InheritableThreadLocal
。
# 5.1 InheritableThreadLocal
介绍
InheritableThreadLocal
是 ThreadLocal
的一个子类。当父线程创建一个子线程时,InheritableThreadLocal
会自动将父线程中存储的值复制一份给子线程。
关键特性:
- 子线程创建时,会继承父线程在
InheritableThreadLocal
变量上的值。 - 这种继承是值复制 (浅拷贝),不是共享引用。子线程后续对该值的修改不会影响父线程。
- 父线程在子线程创建之后对
InheritableThreadLocal
值的修改,不会影响已经创建的子线程。
# 5.2 ThreadLocal
vs InheritableThreadLocal
对比
# ThreadLocal
在父子线程中的表现
/**
* 演示 ThreadLocal 在父子线程中的行为
*/
public class ThreadLocalParentChildDemo {
// 创建一个普通的 ThreadLocal
private static ThreadLocal<String> normalThreadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
// 主线程 (父线程) 设置值
normalThreadLocal.set("Parent Value - Normal");
System.out.println("Parent thread gets: " + normalThreadLocal.get()); // 输出: Parent Value - Normal
// 创建并启动子线程
Thread childThread = new Thread(() -> {
// 子线程尝试获取值
System.out.println("Child thread gets: " + normalThreadLocal.get()); // 输出: null
// 子线程设置自己的值
normalThreadLocal.set("Child Value - Normal");
System.out.println("Child thread sets and gets: " + normalThreadLocal.get()); // 输出: Child Value - Normal
}, "ChildThread-Normal");
childThread.start();
childThread.join(); // 等待子线程结束
// 父线程再次获取值,不受子线程影响
System.out.println("Parent thread gets again: " + normalThreadLocal.get()); // 输出: Parent Value - Normal
normalThreadLocal.remove(); // 清理
}
}
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
# InheritableThreadLocal
在父子线程中的表现
/**
* 演示 InheritableThreadLocal 在父子线程中的行为
*/
public class InheritableThreadLocalParentChildDemo {
// 创建一个 InheritableThreadLocal
private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
// 如果需要自定义继承时的值处理逻辑,可以重写 childValue 方法
private static InheritableThreadLocal<StringBuilder> customInheritable = new InheritableThreadLocal<StringBuilder>() {
@Override
protected StringBuilder childValue(StringBuilder parentValue) {
// 当子线程创建时,此方法被调用以决定子线程的值
// 这里返回父值的副本,并追加 "(copied)"
return parentValue == null ? null : new StringBuilder(parentValue).append("(copied)");
}
};
public static void main(String[] args) throws InterruptedException {
// --- 基本继承 ---
System.out.println("--- Basic Inheritance ---");
// 父线程设置值
inheritableThreadLocal.set("Parent Value - Inheritable");
System.out.println("Parent thread gets: " + inheritableThreadLocal.get()); // 输出: Parent Value - Inheritable
// 创建并启动子线程
Thread childThread1 = new Thread(() -> {
// 子线程获取值 (继承自父线程)
System.out.println("Child thread 1 gets: " + inheritableThreadLocal.get()); // 输出: Parent Value - Inheritable
// 子线程修改自己的值
inheritableThreadLocal.set("Child Value - Inheritable");
System.out.println("Child thread 1 sets and gets: " + inheritableThreadLocal.get()); // 输出: Child Value - Inheritable
}, "ChildThread-Inheritable-1");
childThread1.start();
childThread1.join();
// 父线程再次获取值,不受子线程影响
System.out.println("Parent thread gets again: " + inheritableThreadLocal.get()); // 输出: Parent Value - Inheritable
inheritableThreadLocal.remove();
// --- 演示父线程后续修改不影响已创建的子线程 ---
System.out.println("\n--- Parent Modification After Child Creation ---");
inheritableThreadLocal.set("Parent Value - Before Child 2");
Thread childThread2 = new Thread(() -> {
System.out.println("Child thread 2 gets (initially): " + inheritableThreadLocal.get()); // 输出: Parent Value - Before Child 2
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Child thread 2 gets (later): " + inheritableThreadLocal.get()); // 仍然是: Parent Value - Before Child 2
}, "ChildThread-Inheritable-2");
childThread2.start();
Thread.sleep(50); // 确保子线程已启动并获取了初始值
inheritableThreadLocal.set("Parent Value - After Child 2 Started"); // 父线程修改值
System.out.println("Parent thread gets after modification: " + inheritableThreadLocal.get()); // 输出: Parent Value - After Child 2 Started
childThread2.join();
inheritableThreadLocal.remove();
// --- 演示自定义 childValue ---
System.out.println("\n--- Custom childValue Inheritance ---");
StringBuilder parentSb = new StringBuilder("ParentSB");
customInheritable.set(parentSb);
System.out.println("Parent thread gets StringBuilder: " + customInheritable.get());
Thread childThread3 = new Thread(() -> {
System.out.println("Child thread 3 gets StringBuilder: " + customInheritable.get()); // 输出: ParentSB(copied)
// 修改子线程的值
customInheritable.get().append("-ChildModified");
System.out.println("Child thread 3 modified value: " + customInheritable.get());
}, "ChildThread-Custom");
childThread3.start();
childThread3.join();
// 父线程的值未受影响
System.out.println("Parent thread gets StringBuilder again: " + customInheritable.get()); // 输出: ParentSB
customInheritable.remove();
}
}
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
# 5.3 InheritableThreadLocal
工作原理
InheritableThreadLocal
利用了 Thread
类中的另一个 ThreadLocalMap
成员变量:inheritableThreadLocals
。
// Thread 类内部
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
2
当使用 InheritableThreadLocal
的 set/get/remove
方法时,实际操作的是当前线程的 inheritableThreadLocals
这个 Map。
关键在于线程创建:
当一个新的 Thread
对象被创建时,在其 init
方法(由构造函数调用)中,会检查父线程 (parent
) 的 inheritableThreadLocals
是否为 null
并且是否需要继承 (inheritThreadLocals
标志位,默认为 true
)。如果需要继承,则会调用 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)
来创建一个子线程的 inheritableThreadLocals
Map。
// Thread 类 init 方法的部分逻辑 (简化)
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
// ... 其他初始化 ...
Thread parent = currentThread(); // 获取父线程
// ...
if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
// 如果需要继承且父线程有可继承的 Map
// 则为子线程创建一个新的 Map,其内容基于父线程的 Map
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
// ...
}
// ThreadLocal.createInheritedMap 静态方法 (简化逻辑)
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
// 创建一个新的 ThreadLocalMap,其初始内容是父 Map 中每个 Entry 的副本
// 对于每个 Entry,会调用 InheritableThreadLocal 的 childValue 方法来决定子线程的值
return new ThreadLocalMap(parentMap);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
createInheritedMap
的过程大致是遍历父 Map 的所有 Entry,对于每个 Entry,获取其 Key (InheritableThreadLocal
实例) 和 Value。然后调用 Key (即 InheritableThreadLocal
对象) 的 childValue(parentValue)
方法,用其返回值作为子线程 Map 中该 Key 对应的新 Value。默认的 childValue
方法直接返回 parentValue
(浅拷贝)。
# 5.4 InheritableThreadLocal
在线程池中的局限性
InheritableThreadLocal
最大的问题在于它与线程池一起使用时。线程池的核心机制是线程复用。
- 当一个任务提交给线程池时,它会被一个池中的工作线程执行。
- 这个工作线程可能之前已经执行过其他任务。
InheritableThreadLocal
的值传递只发生在线程创建时 (new Thread()
)。- 线程池中的线程一旦创建,就不会再重新执行
init
方法来从“父线程”(即提交任务的那个线程)那里继承ThreadLocal
值了。
后果:
- 无法获取最新值: 如果提交任务的线程在提交第一个任务后,修改了
InheritableThreadLocal
的值,然后提交第二个任务到同一个工作线程,第二个任务获取到的仍然是工作线程第一次被创建时从当时的父线程继承来的旧值,而不是提交任务线程的最新值。 - 数据污染: 工作线程执行完一个任务后,其
inheritableThreadLocals
Map 中的值会保留下来。如果这个线程接着执行另一个任务,而这个新任务期望的上下文与旧值不同,就会发生数据污染。虽然remove()
可以解决部分问题,但无法解决无法获取提交者最新值的问题。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 演示 InheritableThreadLocal 在线程池中的限制
*/
public class InheritableThreadPoolLimitationDemo {
// 定义 InheritableThreadLocal
private static InheritableThreadLocal<String> context = new InheritableThreadLocal<>();
// 创建一个单线程的线程池,确保线程被复用
private static ExecutorService threadPool = Executors.newFixedThreadPool(1);
public static void main(String[] args) throws InterruptedException {
System.out.println("Main thread starts.");
// 主线程设置初始值
context.set("Value from Main - Task 1");
System.out.println("Main thread sets context: " + context.get());
// 提交第一个任务
threadPool.submit(() -> {
// 工作线程获取的值是提交任务时主线程的值 (通过线程创建时继承,这里模拟)
System.out.println("Worker thread (Task 1) gets context: " + context.get()); // 输出: Value from Main - Task 1
// 工作线程修改自己的值
context.set("Value set by Worker in Task 1");
System.out.println("Worker thread (Task 1) sets context: " + context.get());
// 此处没有调用 remove(),模拟值残留
});
// 等待第一个任务执行完毕
TimeUnit.MILLISECONDS.sleep(100); // 简单等待,实际应使用 Future 等待
// 主线程修改值
context.set("Value from Main - Task 2");
System.out.println("\nMain thread updates context: " + context.get());
// 提交第二个任务 (复用同一个工作线程)
threadPool.submit(() -> {
// 工作线程获取的值是上一个任务残留的值,而不是主线程的最新值!
System.out.println("Worker thread (Task 2) gets context: " + context.get()); // !! 输出: Value set by Worker in Task 1 !!
// !! 而不是 "Value from Main - Task 2" !!
});
// 关闭线程池
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.SECONDS);
System.out.println("\nMain thread finished.");
context.remove(); // 清理主线程的
}
}
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
# 6. TransmittableThreadLocal (TTL):线程池上下文传递的终极方案
为了解决 InheritableThreadLocal
在线程池等异步执行场景下的上下文传递问题,阿里巴巴开源了 TransmittableThreadLocal
(TTL)。
# 6.1 TTL 介绍
TTL 是一个旨在增强 InheritableThreadLocal
的库,它确保在使用线程池、@Async
注解、CompletableFuture
等异步执行模型时,能够可靠地将父线程(提交任务的线程)的 ThreadLocal
上下文传递给子线程(执行任务的线程)。
核心目标: 让开发者感觉就像在使用普通的 InheritableThreadLocal
一样简单,但在异步场景下也能正确工作。
# 6.2 TTL 工作原理
TTL 的实现比 InheritableThreadLocal
更复杂,它通过装饰 (Decorate) Java 标准库中的相关类(如 Runnable
, Callable
, ExecutorService
)来实现上下文的捕获和传递:
- 上下文捕获 (Capture): 在提交任务时(例如调用
ExecutorService.submit()
之前),TTL 会捕获当前线程中所有TransmittableThreadLocal
实例及其对应的值,形成一个“快照 (Snapshot)”。 - 上下文传递 (Transmit): 这个“快照”会与被提交的任务(如
Runnable
或Callable
)关联起来。TTL 提供了包装类(如TtlRunnable
,TtlCallable
)来持有这个快照。 - 上下文回放 (Replay / Restore): 在任务执行时(即在线程池的工作线程中执行
run()
或call()
方法之前),TTL 的包装类会执行replay
操作:将快照中的ThreadLocal
值设置到当前工作线程的ThreadLocalMap
中。 - 上下文恢复 (Backup / Restore): 在任务执行完毕后(
run()
或call()
方法执行结束,无论成功还是异常),TTL 会执行restore
操作:恢复工作线程在执行任务之前的原始ThreadLocal
状态(清除掉 replay 进去的值,恢复原来的值)。
时序流程示意:
这种机制确保了:
- 执行任务的线程能获取到提交任务时的上下文信息。
- 任务执行期间对
ThreadLocal
的修改不会污染线程池中的线程,因为执行完后会恢复原状。
# 6.3 TTL 基本使用
首先,需要添加 TTL 的 Maven 依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<!-- 使用最新稳定版本 -->
<version>2.14.5</version>
</dependency>
2
3
4
5
6
然后,将需要传递的 ThreadLocal
变量定义为 TransmittableThreadLocal
类型,并在提交异步任务时使用 TTL 提供的包装器。
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable; // TTL 提供的 Runnable 包装类
import com.alibaba.ttl.TtlCallable; // TTL 提供的 Callable 包装类
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* 演示 TransmittableThreadLocal 在线程池中的使用
*/
public class TransmittableThreadPoolDemo {
// 1. 定义 TransmittableThreadLocal 变量
private static TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// 创建线程池
private static ExecutorService threadPool = Executors.newFixedThreadPool(1);
public static void main(String[] args) throws Exception {
System.out.println("Main thread starts.");
// 主线程设置初始值
context.set("Value from Main - Task 1 (TTL)");
System.out.println("Main thread sets context: " + context.get());
// 2. 使用 TtlRunnable.get() 包装 Runnable 任务
Runnable task1 = () -> {
// 工作线程能正确获取到提交任务时主线程的值
System.out.println("Worker thread (Task 1) gets context: " + context.get()); // 输出: Value from Main - Task 1 (TTL)
context.set("Value set by Worker in Task 1 (TTL)"); // 工作线程修改自己的上下文
System.out.println("Worker thread (Task 1) sets context: " + context.get());
};
Runnable ttlTask1 = TtlRunnable.get(task1); // 获取包装后的 Runnable
// 提交包装后的任务
threadPool.submit(ttlTask1);
TimeUnit.MILLISECONDS.sleep(100); // 等待任务 1 完成
// 主线程修改值
context.set("Value from Main - Task 2 (TTL)");
System.out.println("\nMain thread updates context: " + context.get());
// 3. 使用 TtlCallable.get() 包装 Callable 任务
Callable<String> task2 = () -> {
// 工作线程能正确获取到提交任务时主线程的最新值
String receivedValue = context.get();
System.out.println("Worker thread (Task 2) gets context: " + receivedValue); // 输出: Value from Main - Task 2 (TTL)
return "Task 2 result, received: " + receivedValue;
};
Callable<String> ttlTask2 = TtlCallable.get(task2); // 获取包装后的 Callable
// 提交包装后的任务并获取结果
Future<String> future = threadPool.submit(ttlTask2);
System.out.println("Task 2 Future result: " + future.get());
// 4. 验证线程池线程状态是否被恢复 (提交一个普通任务)
threadPool.submit(() -> {
System.out.println("\nWorker thread (Plain Task) gets context after TTL tasks: " + context.get()); // 输出: null (或初始值),证明 TTL 成功恢复了线程状态
});
// 关闭线程池
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.SECONDS);
System.out.println("\nMain thread finished.");
context.remove(); // 清理主线程的
}
}
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
输出结果对比: 可以看到,使用 TTL 后,任务 2 能够正确获取到主线程更新后的值 Value from Main - Task 2 (TTL)
,并且普通任务运行时线程的上下文是干净的 (null
),证明了 TTL 的有效性。
# 6.4 TTL 进阶:装饰线程池与自定义传递
1. 装饰 ExecutorService
(推荐)
手动包装每个 Runnable/Callable
比较繁琐。TTL 提供了工具类 TtlExecutors
来直接装饰(包装)整个 ExecutorService
实例。之后通过这个装饰后的 ExecutorService
提交任务,就无需再手动包装 Runnable/Callable
了,TTL 会自动处理。
import com.alibaba.ttl.threadpool.TtlExecutors; // 用于装饰线程池的工具类
// ... 其他 import ...
public class TtlExecutorDecoratorDemo {
private static TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// 创建原始线程池
private static ExecutorService originalExecutor = Executors.newFixedThreadPool(1);
// 使用 TtlExecutors 装饰原始线程池
private static ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(originalExecutor);
public static void main(String[] args) throws Exception {
context.set("Value via Decorated Executor");
System.out.println("Main thread context: " + context.get());
// 直接使用装饰后的 ttlExecutor 提交【未经包装】的 Runnable
ttlExecutor.submit(() -> {
System.out.println("Worker thread (via decorated executor) gets context: " + context.get());
// 输出: Value via Decorated Executor
});
// 关闭原始线程池和装饰后的线程池(通常只需关闭原始的)
originalExecutor.shutdown();
originalExecutor.awaitTermination(1, TimeUnit.SECONDS);
System.out.println("\nMain thread finished.");
context.remove();
}
}
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
注意: TTL 还支持对 ForkJoinPool
, TimerTask
等进行类似的装饰。
2. 自定义值的复制逻辑 (深拷贝)
InheritableThreadLocal
和 TransmittableThreadLocal
默认都是进行浅拷贝。如果 ThreadLocal
中存储的是可变对象(如 List
, Map
, 自定义对象),父子线程(或任务提交者与执行者)获取到的是指向同一个可变对象的引用(或其浅拷贝)。一方修改会影响另一方。
如果需要深拷贝(即子线程/任务执行者获取到一个完全独立的对象副本),可以重写 TransmittableThreadLocal
的 copyValue
方法 (类似于 InheritableThreadLocal
的 childValue
)。
import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.HashMap;
import java.util.Map;
/**
* 演示 TTL 自定义深拷贝逻辑
*/
public class TtlDeepCopyDemo {
// 定义一个存储可变对象 (Map) 的 TTL,并重写 copyValue 实现深拷贝
private static TransmittableThreadLocal<Map<String, String>> mapContext =
new TransmittableThreadLocal<Map<String, String>>() {
/**
* 在捕获上下文时调用此方法,用于创建值的副本。
* @param parentValue 父线程(或提交任务线程)的值
* @return 要传递给子线程(或执行任务线程)的值副本
*/
@Override
protected Map<String, String> copyValue(Map<String, String> parentValue) {
System.out.println("Copying map for transmission...");
if (parentValue == null) {
return null;
}
// 创建一个新的 HashMap,实现深拷贝
return new HashMap<>(parentValue);
}
};
public static void main(String[] args) {
// 主线程设置 Map
Map<String, String> mainMap = new HashMap<>();
mainMap.put("key1", "value1");
mapContext.set(mainMap);
System.out.println("Main thread initial map: " + mapContext.get());
// 启动一个新线程 (TTL 对 Thread 也有效,如果父线程的 mapContext 非 null)
Thread childThread = new Thread(() -> {
Map<String, String> childMap = mapContext.get();
System.out.println("Child thread received map: " + childMap);
// 修改子线程的 Map
childMap.put("key2", "value2_child");
System.out.println("Child thread modified map: " + childMap);
});
childThread.start();
try { childThread.join(); } catch (InterruptedException e) {}
// 验证主线程的 Map 是否受影响
System.out.println("Main thread map after child modification: " + mapContext.get());
// 输出: {key1=value1} (未受影响,因为传递的是深拷贝)
mapContext.remove();
}
}
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
# 7. ThreadLocal 使用总结与建议
ThreadLocal
及其变种是处理线程数据隔离的强大工具,但理解其工作原理和潜在陷阱至关重要。
核心要点回顾:
- 作用: 提供线程本地变量,实现线程间数据隔离,避免同步开销。核心思想是“空间换时间”。
- 原理: 每个
Thread
对象持有ThreadLocalMap
,Key 是ThreadLocal
对象的弱引用,Value 是实际存储的值。 - 内存泄漏: 主要风险在于
ThreadLocalMap
生命周期与线程绑定,且 Key 被回收后 Value 仍被强引用。必须通过显式调用remove()
方法来避免。 InheritableThreadLocal
: 允许子线程在创建时继承父线程的值(浅拷贝),但在线程池复用场景下失效。TransmittableThreadLocal
(TTL): 解决了InheritableThreadLocal
在线程池等异步场景下的问题,通过上下文捕获、传递和恢复机制,确保上下文正确传递。推荐在异步环境中使用。
使用建议:
- 明确目的: 确认你的场景确实需要线程隔离的数据,而不是需要线程间共享和同步的数据。
- 强制
remove()
: 养成在使用完ThreadLocal
(特别是ThreadLocal
和InheritableThreadLocal
) 后调用remove()
的习惯,使用try-finally
保证执行。 - 线程池场景优先 TTL: 在涉及线程池、
@Async
、CompletableFuture
等异步执行模型时,优先使用TransmittableThreadLocal
并考虑装饰ExecutorService
。 - 谨慎存储大对象:
ThreadLocal
不适合存储生命周期长且占用内存大的对象,否则可能加剧内存消耗和泄漏风险。 - 注意可变对象: 如果存储的是可变对象,默认的浅拷贝(
InheritableThreadLocal
/ TTL)可能导致意想不到的副作用。如果需要完全隔离,应实现深拷贝逻辑(重写childValue
/copyValue
)。 - 命名清晰: 给
ThreadLocal
变量起一个能清晰反映其用途的名字。 - 封装使用: 可以将
ThreadLocal
的操作封装在工具类(如UserContextHolder
)中,统一管理set
,get
,remove
逻辑,降低直接操作ThreadLocal
的风险。
通过遵循这些原则和最佳实践,你可以安全、高效地利用 ThreadLocal
及其相关技术来简化并发编程,提升应用程序的性能和可维护性。