一、ThreadLocal
1.1 定义与工作原理
-
ThreadLocal 是 Java 提供的一种线程局部变量机制,解决多线程共享变量引发的并发问题。
-
每个线程内部维护一个
ThreadLocalMap,Key 是 ThreadLocal 对象本身(弱引用),Value 是线程独享的变量副本(强引用)。 -
多线程同时访问某个 ThreadLocal 实例时,各自线程访问的是自己副本,无需加锁即可实现线程安全。
内部结构图示(简化)
Thread
└── ThreadLocalMap
├── [ThreadLocal_A] -> ValueA
└── [ThreadLocal_B] -> ValueB
-
Key(ThreadLocal 实例)为弱引用,Value 为强引用
-
若 ThreadLocal 实例被 GC 丢失引用,value 无法被主动清除,可能导致内存泄漏
ThreadLocalMap 内部机制
-
ThreadLocalMap 是 ThreadLocal 的静态内部类,采用开放寻址法解决 Hash 冲突
-
key 是弱引用 WeakReference<ThreadLocal<?>>,value 是强引用
-
Entry 不会自动清理,若 key 被 GC 回收,则 key=null 的 entry 仍占用内存
-
清理依赖显式调用 remove() 或 set() 时触发 expungeStaleEntry()
1.2 应用场景与实战
应用场景
-
用户上下文传递(如 userId、tenantId、token)
-
TraceId 日志链路追踪
-
Spring 事务绑定(Connection 保存在 ThreadLocal 中)
-
线程级缓存(如非线程安全的 SimpleDateFormat)
项目中典型用法
@Slf4j
public class TraceContextHolder {
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
public static void set(String traceId) {
TRACE_ID.set(traceId);
}
public static String get() {
return TRACE_ID.get();
}
public static void clear() {
TRACE_ID.remove();
}
}
-
在网关或拦截器设置 TraceId,在日志 MDC 中输出,全链路追踪
示例:Spring 中事务上下文的 ThreadLocal
// Spring 事务管理器内部
public class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
...
}
-
每个线程绑定当前事务的资源(Connection、状态等),实现隐式事务传递
1.3 问题总结与注意事项
| 问题点 | 原因分析与风险说明 |
|---|---|
| 内存泄漏 | key 为弱引用,value 为强引用,未调用 remove() 会导致无法回收 |
| 数据串用 | 线程池复用线程,旧请求的数据未清理干净,污染新请求上下文 |
| 子线程不可见 | ThreadLocal 是线程私有数据结构,子线程无法访问父线程 ThreadLocalMap |
| 清理不及时 | 依赖手动调用 remove(),没有自动清理机制,尤其在线程池场景风险大 |
二、volatile
2.1 原理
-
volatile是 JVM 提供的轻量级同步机制,用于解决可见性问题 -
修饰变量后,对变量的写操作会立即刷新主内存,其他线程可立即读取最新值
-
禁止 JVM 对指令重排序(仅限于 volatile 写操作之前的指令不会被 reorder 到 volatile 之后)
2.2 与原子性区别
private volatile int count = 0;
public void add() {
count++; // 非原子性,读-改-写三个步骤并非原子执行
}
-
volatile 无法解决竞争条件(race condition)
-
如需保证原子性需使用:
-
synchronized/ReentrantLock -
AtomicInteger/LongAdder
-
2.3 应用场景
-
控制线程关闭:
private volatile boolean stop = false; while (!stop) { ... } -
双重检查锁(DCL)单例
private static volatile Singleton instance; if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } }
三、ThreadLocal vs volatile
| 对比点 | ThreadLocal | volatile |
| 用途 | 每个线程持有独立变量副本 | 多线程共享变量(可见性保障) |
| 线程隔离 | 是 | 否 |
| 是否加锁 | 不需要 | 不需要(非原子性) |
| 是否原子 | 与是否共享无关 | 否,需要结合 CAS/锁使用 |
| 风险 | 内存泄漏,线程池复用污染 | 无法保证原子性 |
四、子线程事务一致性问题
4.1 问题背景
-
Spring 使用
ThreadLocal保存事务上下文(TransactionSynchronizationManager) -
子线程默认拿不到主线程 ThreadLocal,导致事务断裂
4.2 真实案例
@Transactional
public void create() {
db.insertOrder();
executorService.submit(() -> {
// 子线程无法复用主线程的连接/事务
db.insertLog(); // 与主线程事务隔离
});
}
4.3 正确做法
| 方法 | 适用与说明 |
| 1)避免子线程操作事务 | 子线程做 MQ、通知等非事务工作 |
| 2)手动传递连接、事务对象 | 侵入性强,维护复杂,不推荐 |
| 3)使用 TransmittableThreadLocal | 保证上下文 ThreadLocal 可穿透线程池 |
| 4)使用事务消息方案 | 采用最终一致性设计思路(TCC/补偿) |
五、内存泄漏案例解析
5.1 模拟 ThreadLocal 内存泄漏
class Leaky {
private static final ThreadLocal<byte[]> local = new ThreadLocal<>();
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
local.set(new byte[1024 * 1024]); // 每次分配 1MB
}
System.gc();
}
}
-
若 local 被回收但线程未结束,value 无法释放,造成泄漏
5.2 内存泄漏排查建议
-
使用 jmap + MAT/Eclipse Memory Analyzer 排查堆中大量 value 占用情况
-
定位某线程 ThreadLocalMap 残留 Entry(key=null, value≠null)
-
定期在线程生命周期内调用
remove()
六、面试答题强化模板
Q1:ThreadLocal 是什么?有什么典型使用与问题?
ThreadLocal 提供线程局部变量副本,常用于上下文传递、连接绑定、日志追踪。 问题是内存泄漏(未 remove)、线程池复用污染、子线程不可见。
Q2:volatile 有什么限制?如何解决原子性问题?
volatile 只保证内存可见性与指令顺序,但无法解决原子性。 可结合 CAS 原子类(如 AtomicInteger),或使用 synchronized。
Q3:Spring 子线程为什么拿不到事务?如何解决?
因事务信息保存在主线程的 ThreadLocalMap 中,子线程无法访问。 推荐用 TransmittableThreadLocal、事件通知或 TCC 等模式解耦。

2745

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



