ThreadLocal 是 Java 中实现线程局部变量的重要工具,它能让每个线程拥有自己独立的变量副本,从而在多线程环境下避免共享数据的竞争问题。下面从作用、使用、实现原理、注意事项四个方面详细说明。
1. 作用
- 线程隔离:每个线程通过同一个 ThreadLocal 对象存取数据时,实际操作的是各自线程内部的存储空间,互不干扰。
- 简化并发:无需加锁就能保证线程安全,适合存储线程上下文信息。
- 避免参数传递:可以将一些公共信息(如用户身份、事务连接、日志追踪 ID)放入 ThreadLocal,在同一个线程的任意位置获取,避免层层传参。
2. 基本使用
java
dart
// 创建 ThreadLocal 对象,可指定初始值
ThreadLocal<String> local = ThreadLocal.withInitial(() -> "default");
// 线程 A 中设置
local.set("valueA");
// 线程 B 中设置
local.set("valueB");
// 各自获取
String a = local.get(); // 线程 A 得到 "valueA"
String b = local.get(); // 线程 B 得到 "valueB"
// 使用完后建议移除,避免内存泄漏
local.remove();
常用方法:
set(T value):为当前线程设置值。get():获取当前线程的值,若未设置则返回初始值(通过initialValue()或withInitial指定)。remove():移除当前线程的值,释放资源。initialValue():初始化方法(一般通过withInitial或重写该方法)。
3. 典型应用场景
- 数据库连接 / 事务管理 :每个线程持有自己的
Connection对象,确保事务隔离。 - 用户会话 / 请求上下文:在 Web 应用中,将用户信息存入 ThreadLocal,同一请求的 Service、DAO 层可直接获取。
- 线程安全的日期格式化 :
SimpleDateFormat非线程安全,每个线程使用自己的实例。 - 日志追踪 ID:为每个请求生成唯一 ID,存入 ThreadLocal,方便日志串联。
4. 实现原理(核心)
ThreadLocal 的实现依赖于每个线程内部的 ThreadLocalMap。
关键点:
- 每个线程对象(
Thread)都有一个字段threadLocals,类型为ThreadLocalMap。 ThreadLocalMap是一个自定义的哈希表,其 Entry 的 key 是 ThreadLocal 实例的弱引用,value 是用户存储的值。- 当调用
set()时,获取当前线程的ThreadLocalMap,将当前 ThreadLocal 对象作为 key,传入值作为 value 存入。 - 当调用
get()时,同样根据当前线程的 Map 和当前 ThreadLocal key 取出 value;若 Map 不存在则初始化,若 key 不存在则返回初始值。 - 由于 key 是弱引用,当 ThreadLocal 实例不再被强引用时,在 GC 时 key 会被回收,但 value 仍被 Entry 强引用,可能造成内存泄漏。
源码简览:
java
ini
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
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();
}
5. 注意事项与内存泄漏
5.1 内存泄漏风险
- 原因 :线程池中的线程会长期存活,如果
ThreadLocal对象被回收(弱引用),但 Entry 中的 value 仍被线程的ThreadLocalMap强引用,导致 value 无法被回收,造成内存泄漏。 - 表现:线程池中线程复用,长时间运行可能导致内存占用持续增长。
5.2 解决方案
-
务必调用
remove():在线程执行完任务后,显式调用remove()清除当前线程的变量。尤其在 Web 容器(如 Tomcat)中使用线程池时,必须养成习惯。 -
使用
try-finally结构:java
csharptry { local.set(value); // 业务逻辑 } finally { local.remove(); }
5.3 其他注意事项
- 线程池环境 :由于线程会被复用,若未及时
remove(),下一个任务可能读取到旧值,导致业务错误。 - 不恰当的使用:避免用 ThreadLocal 传递大对象,增加内存压力。
- 父子线程传递 :
InheritableThreadLocal可以实现子线程继承父线程的值,但需谨慎使用。
6. 总结
- ThreadLocal 提供了线程隔离的变量副本,简化并发编程。
- 底层通过
ThreadLocalMap实现,key 为弱引用,value 为强引用。 - 使用后必须
remove(),尤其是在线程池环境中,以防止内存泄漏和脏数据问题。 - 适用于存储线程上下文、连接对象、非线程安全工具类的实例等。