ThreadLocal
- ThreadLocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。
- 核心意义在于为每个线程提供独立的变量副本,实现线程间数据的隔离与安全共享。
一、ThreadLocal的简单使用
static final ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
sThreadLocal.set();
sThreadLocal.get();
二、ThreadLocal原理

在Thread类中,有一个成员变量threadLocals
class Thread implements Runnable {
......
//该变量后续调用的时候会有方法创建并示例化
Threadlocal.ThreadLocalMap threadLocals = null;
......
}
也就是说,每个实例化的Thread对象,内部都带有一个成员变量threadLocals。
然后再通过ThreadLocal类来进行封装修饰,每次对ThreadLocal对象的操作,其实都是对线程内的ThreadLocalMap类型的threadLocals变量进行操作。
ThreadLocal对象可以理解为一个商品中介,线程A可以理解为A公司总部,Threadlocals变量可以理解为A公司旗下的子公司。
例子:有一天,A公司总部(线程A)想买一些原材料,于是找商品中介(ThreadLocal对象)商谈说要买XX吨原材料,而这个商品中介比较奸商,竟然找到A公司旗下的子公司(Threadlocals变量),向其购买XX吨原材料,自己赚中介费。最后需求方跟生产方都是A公司集团内,商品中介就是一个沟通的桥梁。
---------------------------------------------------------------------
因为Java内存模型的设计,所以每个线程内的成员变量都是互不干扰,相互独立。不会因为修改了线程A而导致线程B的数据被修改,而且数据的服务区域也只限于该线程内。这种特性跟我们平时认识的ThreadLocal特性是一样的。
备注:
- ThreadLocalMap是ThreadLocal的一个内部类,是一个定制的哈希映射,仅适用于维护线程本地值。ThreadLocalMap类是包私有的,允许在Thread类中声明字段。为了帮助处理非常大且长时间的使用,哈希表entry使用了对键的弱引用,有助于GC回收。
三、 一个线程调用多个ThreadLocal如何防止冲突
//在某一线程声明了ABC三种类型的ThreadLocal
ThreadLocal<A> sThreadLocalA = new ThreadLocal<A>();
ThreadLocal<B> sThreadLocalB = new ThreadLocal<B>();
ThreadLocal<C> sThreadLocalC = new ThreadLocal<C>();
为了确定ABC在threadLocalMap的table中位置,以及能正常访问对应的值,有一种方法计算出确定的索引值。
private void set(ThreadLocal<?> key, Object value) {
......
//获取索引值,这个地方是比较特别的地方
int i = key.threadLocalHashCode & (len-1);
......
}
/**
*ThreadLocal中threadLocalHashCode相关代码.
**/
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);
}
因为static原因,在每次new ThreadLocal时因为ThreadLocalHashCode的初始化,会使threadLocalHashCode值自增一次,增值为0x61c88647。
0x61c88是斐波那契散列乘数,它的优点是通过它散列(hash)出来的结果分布比较均匀,可以很大程度上避免hash冲突
总结:
(1)对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。
(2)对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。
四、ThreadLocal特性
ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是:
(1)Synchronized是通过线程等待,牺牲时间来解决访问冲突。
(2)ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相对于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
五、Threadlocal内存泄漏问题是怎么导致的,如何防止内存泄漏
5-1)内存泄漏原因:
5-1-1)Entry 的键值设计:ThreadLocalMap 中的 Entry 使用 弱引用(WeakReference) 持有 ThreadLocal 实例作为键(Key),而值(Value)是强引用。
当 ThreadLocal 实例的强引用被置为 null 时,下次垃圾回收(GC)会回收该键(Key 变为 null),但值(Value)仍被 Entry 强引用,无法被回收。
5-1-2)线程生命周期问题:若线程长时间运行(如线程池中的线程),且未主动清理 ThreadLocalMap 中 Key 为 null 的 Entry,这些 Entry 的值会持续占用内存,导致泄漏。
5-1-3)清理机制依赖:ThreadLocalMap 在调用 set()、get() 或 remove() 时,会清理 Key 为 null 的 Entry。但若长期不触发这些操作,无效 Entry 会积累。
5-2)防止内存泄漏的方法:
5-2-1)主动调用 remove() 方法(最重要):使用完 ThreadLocal 后,务必调用 remove() 清理当前线程的 Entry。推荐在 try-finally 块中确保执行:
try {
threadLocal.set(value);
// ... 使用 threadLocal
} finally {
threadLocal.remove(); // 强制清理
}
5-2-2)避免长生命周期的线程:对于线程池等复用线程的场景,需格外注意清理,因为线程的 ThreadLocalMap 会持续存在。
5-2-3)合理设计 ThreadLocal 生命周期:尽量使用 private static 修饰 ThreadLocal 实例,避免频繁创建。但需配合 remove() 使用,否则可能导致泄漏。
5-2-4)避免 Value 强引用其他对象:如果 Value 引用了大对象或外部资源(如数据库连接),需及时释放。
六、Threadlocal参数如何传递,线程池如何传递
6-1)父子线程间的传递:
默认情况下,子线程无法直接访问父线程的 ThreadLocal 变量。若需传递,需使用 InheritableThreadLocal。
局限性:
- 线程池场景失效:线程池中的线程是复用的,不会重新执行初始化逻辑,导致后续任务无法继承新的父线程值。
- 数据污染:若线程池中的线程未清理旧值,可能导致不同任务间的数据混乱。
6-2)线程池中的参数传递:
6-2-1)手动传递参数(显式传参)
将参数直接封装到任务对象中,避免依赖隐式上下文。
示例代码
public class ExplicitParamDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
String context = "Task Context Data";
executor.submit(new Task(context));
}
static class Task implements Runnable {
private final String context; // 显式传递参数
public Task(String context) {
this.context = context;
}
@Override
public void run() {
System.out.println("Processing with context: " + context);
}
}
}
优点:无隐式依赖,线程安全。
缺点:代码冗余,需修改任务接口。
6-2-2)使用 TransmittableThreadLocal(阿里开源库)
通过 TransmittableThreadLocal (TTL) 解决线程池上下文传递问题,支持任务提交时自动捕获和恢复上下文。
步骤1:添加依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>
步骤2:使用 TTL 装饰线程池
public class TTLDemo {
private static TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
public static void main(String[] args) {
// 创建线程池并用 TTL 装饰
ExecutorService executor = TtlExecutors.getTtlExecutorService(
Executors.newFixedThreadPool(2)
);
context.set("Main Context");
executor.submit(() -> {
System.out.println("Task1 Context: " + context.get()); // 输出 Main Context
});
context.set("Updated Context");
executor.submit(() -> {
System.out.println("Task2 Context: " + context.get()); // 输出 Updated Context
});
}
}
输出:
Task1 Context: Main Context
Task2 Context: Updated Context
备注:
1)TransmittableThreadLocal总结:
TransmittableThreadLocal(TTL)是阿里开源的一个工具库,其核心目标是解决 线程池场景下 ThreadLocal 上下文传递 的问题。它的源码设计巧妙地扩展了 InheritableThreadLocal,并通过 装饰线程池 和 任务包装 实现跨线程的上下文传递。以下是其核心原理的简化分析:
1-1)核心类与继承关系
public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> {
// 核心逻辑
}
- 继承自
InheritableThreadLocal:天然支持父子线程间的值传递。 - 扩展能力:通过增强线程池任务的生命周期管理,支持线程复用场景。
1-2)核心机制
步骤一:注册所有 TTL 实例
所有 TransmittableThreadLocal 实例会被注册到一个全局的 Holder 集合中,用于统一管理上下文值的捕获与恢复:
public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> {
// 所有 TTL 实例的弱引用集合
static WeakHashMap<TransmittableThreadLocal<Object>, ?> holder =
new WeakHashMap<>();
public TransmittableThreadLocal() {
holder.put((TransmittableThreadLocal<Object>) this, null); // 注册实例
}
}
步骤二:上下文值的捕获与回放
- 捕获(Capture)
在任务提交时,捕获当前线程的所有 TTL 值,生成一个快照(Snapshot):
public static class Snapshot {
final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value;
final HashMap<ThreadLocal<Object>, Object> threadLocal2Value;
}
// 捕获当前线程的 TTL 和普通 ThreadLocal 值
public static Snapshot capture() {
HashMap<TransmittableThreadLocal<Object>, Object> ttlMap = new HashMap<>();
for (TransmittableThreadLocal<Object> ttl : holder.keySet()) {
ttlMap.put(ttl, ttl.get()); // 保存所有 TTL 的值
}
// 普通 ThreadLocal 的处理(需手动注册)
return new Snapshot(ttlMap, threadLocalMap);
}
- 回放(Replay)
在任务执行线程中,将快照中的值绑定到当前线程,并返回旧的上下文(用于后续恢复):
public static Snapshot replay(Snapshot captured) {
Snapshot old = new Snapshot();
// 1. 将 captured 的 TTL 值设置到当前线程
for (Map.Entry<TransmittableThreadLocal<Object>, Object> entry : captured.ttl2Value.entrySet()) {
TransmittableThreadLocal<Object> ttl = entry.getKey();
old.ttl2Value.put(ttl, ttl.get()); // 保存旧值
ttl.set(entry.getValue()); // 设置新值
}
// 2. 处理普通 ThreadLocal
return old;
}
- 恢复(Restore)
任务执行完成后,还原执行线程的原始上下文,避免污染后续任务:
public static void restore(Snapshot old) {
// 恢复 TTL 的旧值
for (Map.Entry<TransmittableThreadLocal<Object>, Object> entry : old.ttl2Value.entrySet()) {
TransmittableThreadLocal<Object> ttl = entry.getKey();
ttl.set(entry.getValue());
}
// 恢复普通 ThreadLocal 的旧值
}
步骤三:线程池任务装饰
TTL 通过装饰线程池的 Runnable/Callable 任务,在任务执行前后插入上下文管理逻辑:
public class TtlRunnable implements Runnable {
private final Runnable runnable;
private final Snapshot captured; // 捕获提交时的上下文
public TtlRunnable(Runnable runnable) {
this.runnable = runnable;
this.captured = capture(); // 提交任务时捕获上下文
}
@Override
public void run() {
Snapshot old = replay(captured); // 执行前回放上下文
try {
runnable.run();
} finally {
restore(old); // 执行后恢复原始上下文
}
}
}
通过 TtlExecutors 装饰原生线程池:
ExecutorService executor = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(4));
executor.submit(task); // 实际提交的是 TtlRunnable
1-3)关键设计思想:
- 装饰器模式:通过包装
Runnable/Callable和线程池,无侵入式增强上下文传递能力。 - 快照隔离:每个任务独立保存提交时的上下文快照,避免多任务间的数据竞争。
- 弱引用管理:使用
WeakHashMap管理 TTL 实例,避免内存泄漏。 - 扩展性:支持普通
ThreadLocal的传递(需手动注册),提供ThreadLocalTransmitter工具类。
1-4)性能优化
- 懒加载快照:仅在首次提交任务时生成快照,减少重复捕获的开销。
1-5)总结
TransmittableThreadLocal 通过 装饰线程池任务 + 上下文快照管理,解决了线程池中 ThreadLocal 传递的难题。其源码核心在于:
- 任务提交时:捕获当前线程的所有 TTL 值。
- 任务执行前:将快照值回放到执行线程。
- 任务执行后:恢复执行线程的原始上下文。
这种设计确保了线程池中每个任务的上下文独立性,是高性能异步编程中的重要基础设施。
备注:
1)对象的四种引用(强引用、软引用、弱引用、虚引用介绍)
1-1)强引用:只要引用存在,垃圾回收器永远不会回收。
例子:
Object obj = new Object();
1-2)软引用:非必须引用,内存溢出之前进行回收
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有时候会返回null
这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;
软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
1-3)弱引用:第二次垃圾回收时回收,可以通过如下代码实现
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,
将返回null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的 isEnQueued 方法返回对象是否被垃圾回收器标记。
1-4)虚引用:垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回是否从内存中已经删除
虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引 用。虚引用主要用于检测对象是否已经从内存中删除。
更多java基础总结(适合于java基础学习、java面试常规题):
总结篇(9)---字符串及基本类 (1)字符串及基本类之基本数据类型
总结篇(10)---字符串及基本类 (2)字符串及基本类之java中公共方法及操作
总结篇(12)---字符串及基本类 (4)Integer对象
总结篇(14)---JVM(java虚拟机) (1)JVM虚拟机概括
总结篇(15)---JVM(java虚拟机) (2)类加载器
总结篇(16)---JVM(java虚拟机) (3)运行时数据区
总结篇(17)---JVM(java虚拟机) (4)垃圾回收
总结篇(18)---JVM(java虚拟机) (5)垃圾回收算法
总结篇(19)---JVM(java虚拟机) (6)JVM调优
总结篇(24)---Java线程及其相关(2)多线程及其问题
总结篇(25)---Java线程及其相关(3)线程池及其问题
总结篇(26)---Java线程及其相关(4)ThreadLocal
总结篇(27)---Java并发及锁(1)Synchronized
总结篇(31)---JUC工具类(1)CountDownLatch
1699

被折叠的 条评论
为什么被折叠?



