【JAVA】ThreadLocal深入剖析
ThreadLocal深入剖析
前言
在并发编程中,当多个线程同时操作一个共享变量,就会出现线程安全问题。常见的解决方案是加锁,但锁会带来性能开销,尤其在高并发场景下。今天要讲的 ThreadLocal,提供了另一种思路:让每个线程拥有自己独立的变量副本,从根本上避免竞争。
🏠个人主页:你的主页
文章目录
一、线程安全问题的两种场景
在讨论 ThreadLocal 之前,我们先搞清楚:什么场景该用锁,什么场景该用 ThreadLocal。
1.1 场景一:多线程竞争修改同一个值
场景描述:电商系统的商品秒杀,100 件商品,多个用户同时抢购。
public class SeckillService {
private int stock = 100; // 库存,共享变量
public void seckill() {
if (stock > 0) {
stock--; // 扣减库存
System.out.println(Thread.currentThread().getName() + " 抢购成功,剩余库存:" + stock);
} else {
System.out.println(Thread.currentThread().getName() + " 抢购失败,库存不足");
}
}
}
问题:两个线程同时判断 stock > 0,都通过了,然后都执行 stock--,导致超卖。
线程A:读取 stock = 1,判断 > 0,准备扣减
线程B:读取 stock = 1,判断 > 0,准备扣减
线程A:stock-- → stock = 0
线程B:stock-- → stock = -1 ❌ 超卖了!
解决方案:加锁
public synchronized void seckill() {
if (stock > 0) {
stock--;
System.out.println(Thread.currentThread().getName() + " 抢购成功");
}
}
能用 ThreadLocal 吗?不能!
因为 ThreadLocal 会给每个线程创建独立副本,每个线程都有自己的 stock = 100,那每个人都能抢 100 件,这显然不对。
结论:当多个线程需要竞争修改同一个值时,必须用锁,不能用 ThreadLocal。
1.2 场景二:每个线程需要独立的上下文数据
场景描述:Web 系统中,每个请求需要携带当前登录用户的信息,在整个请求链路中随时可以获取。
用户A发起请求 → 拦截器解析Token获取用户A信息 → Controller → Service → DAO
用户B发起请求 → 拦截器解析Token获取用户B信息 → Controller → Service → DAO
如果用普通的共享变量存储用户信息:
public class UserContext {
public static User currentUser; // 共享变量
}
问题:
线程A:设置 currentUser = 用户A
线程B:设置 currentUser = 用户B ← 覆盖了!
线程A:获取 currentUser → 拿到的是用户B ❌ 数据错乱!
解决方案:ThreadLocal
public class UserContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
public static void setUser(User user) {
currentUser.set(user);
}
public static User getUser() {
return currentUser.get();
}
}
每个线程都有自己独立的 User 副本,互不干扰。
结论:当每个线程需要独立的上下文数据,且这个数据在整个线程生命周期内需要被多处访问时,用 ThreadLocal。
1.3 两种场景对比
| 场景 | 特点 | 解决方案 |
|---|---|---|
| 秒杀库存 | 多线程竞争修改同一个值 | 加锁(synchronized/Lock) |
| 用户上下文 | 每个线程需要独立的值 | ThreadLocal |
一句话总结:
- 锁:让多个线程排队操作同一个变量
- ThreadLocal:让每个线程各自操作自己的变量
二、ThreadLocal的正确使用场景
2.1 场景一:请求上下文传递
这是 ThreadLocal 最经典的使用场景。
需求:在 Web 应用中,用户登录后,整个请求链路都需要获取当前用户信息。
传统做法:把用户信息作为参数层层传递
// Controller
public void createOrder(User user, OrderDTO orderDTO) {
orderService.createOrder(user, orderDTO);
}
// Service
public void createOrder(User user, OrderDTO orderDTO) {
// 业务逻辑
orderDao.insert(user, order);
logService.log(user, "创建订单");
}
问题:参数传递太繁琐,代码侵入性强。
ThreadLocal 做法:
// 定义用户上下文
public class UserContextHolder {
private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
public static void setUser(User user) {
userHolder.set(user);
}
public static User getUser() {
return userHolder.get();
}
public static void clear() {
userHolder.remove();
}
}
// 拦截器中设置
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
User user = tokenService.parseToken(token);
UserContextHolder.setUser(user); // 存入 ThreadLocal
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserContextHolder.clear(); // 请求结束,清理 ThreadLocal
}
}
// 业务代码中随时获取
public void createOrder(OrderDTO orderDTO) {
User user = UserContextHolder.getUser(); // 直接获取,无需传参
// 业务逻辑...
}
2.2 场景二:数据库连接管理
需求:保证同一个线程内的多次数据库操作使用同一个连接,实现事务。
public class ConnectionManager {
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public static Connection getConnection() throws SQLException {
Connection conn = connectionHolder.get();
if (conn == null) {
conn = dataSource.getConnection();
connectionHolder.set(conn);
}
return conn;
}
public static void closeConnection() {
Connection conn = connectionHolder.get();
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
connectionHolder.remove();
}
}
}
Spring 的 @Transactional 底层就是用 ThreadLocal 来保证同一事务内使用同一个数据库连接。
2.3 场景三:日期格式化工具
问题:SimpleDateFormat 不是线程安全的。
// 错误示范:多线程共享 SimpleDateFormat
public class DateUtil {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
public static String format(Date date) {
return sdf.format(date); // 多线程调用会出问题!
}
}
ThreadLocal 解决:
public class DateUtil {
private static final ThreadLocal<SimpleDateFormat> dateFormatHolder =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static String format(Date date) {
return dateFormatHolder.get().format(date); // 每个线程用自己的实例
}
}
2.4 场景四:链路追踪 TraceId
需求:分布式系统中,一个请求会经过多个服务,需要用 TraceId 串联整个调用链路。
public class TraceContext {
private static final ThreadLocal<String> traceIdHolder = new ThreadLocal<>();
public static void setTraceId(String traceId) {
traceIdHolder.set(traceId);
}
public static String getTraceId() {
return traceIdHolder.get();
}
public static void clear() {
traceIdHolder.remove();
}
}
// 在日志中自动带上 TraceId
public void doSomething() {
log.info("[{}] 开始处理业务", TraceContext.getTraceId());
// 业务逻辑...
}
三、ThreadLocal核心API
3.1 基本方法
ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 设置值
threadLocal.set("hello");
// 获取值
String value = threadLocal.get(); // "hello"
// 移除值(重要!防止内存泄漏)
threadLocal.remove();
3.2 初始值设置
方式一:重写 initialValue 方法
ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
方式二:使用 withInitial(推荐,Java 8+)
ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> 0);
ThreadLocal<List<String>> listHolder = ThreadLocal.withInitial(ArrayList::new);
3.3 完整示例
public class ThreadLocalDemo {
private static final ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
// 线程1
new Thread(() -> {
for (int i = 0; i < 3; i++) {
int value = counter.get();
counter.set(value + 1);
System.out.println(Thread.currentThread().getName() + ": " + counter.get());
}
}, "Thread-A").start();
// 线程2
new Thread(() -> {
for (int i = 0; i < 3; i++) {
int value = counter.get();
counter.set(value + 1);
System.out.println(Thread.currentThread().getName() + ": " + counter.get());
}
}, "Thread-B").start();
}
}
输出(顺序可能不同,但每个线程独立计数):
Thread-A: 1
Thread-A: 2
Thread-A: 3
Thread-B: 1
Thread-B: 2
Thread-B: 3
两个线程各自从 0 开始计数,互不影响。
四、底层数据结构演进
4.1 Java 8 之前的设计
ThreadLocal 对象
└── ThreadLocalMap
├── Entry(Thread-A, value-A)
├── Entry(Thread-B, value-B)
└── Entry(Thread-C, value-C)
特点:
- ThreadLocal 维护一个 Map
- Key 是 Thread 对象
- Value 是变量副本
问题:
- 需要对 Map 加锁,因为多个线程会同时操作这个 Map
- 线程销毁后,对应的 Entry 不会自动清理
4.2 Java 8 之后的设计
Thread-A 对象
└── ThreadLocalMap
├── Entry(ThreadLocal-1, value-1)
├── Entry(ThreadLocal-2, value-2)
└── Entry(ThreadLocal-3, value-3)
Thread-B 对象
└── ThreadLocalMap
├── Entry(ThreadLocal-1, value-1)
└── Entry(ThreadLocal-2, value-2)
特点:
- 每个 Thread 对象内部维护一个 ThreadLocalMap
- Key 是 ThreadLocal 对象
- Value 是变量副本
优势:
- 无需加锁,因为每个线程只操作自己的 Map
- 线程销毁时,ThreadLocalMap 随之销毁,Entry 自动回收
4.3 源码分析
Thread 类中的字段:
public class Thread implements Runnable {
// 每个线程都有自己的 ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocal.set() 方法:
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap
if (map != null) {
map.set(this, value); // Key 是当前 ThreadLocal 对象
} else {
createMap(t, value); // 首次使用,创建 Map
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; // 返回线程的 threadLocals 字段
}
ThreadLocal.get() 方法:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
return (T) e.value;
}
}
return setInitialValue(); // 没有值则返回初始值
}
4.4 ThreadLocalMap 的结构
ThreadLocalMap 是 ThreadLocal 的静态内部类,使用数组 + 线性探测法解决哈希冲突。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // Key 是弱引用
value = v;
}
}
private Entry[] table; // Entry 数组
}
关键点:Entry 的 Key(ThreadLocal 对象)是弱引用。
五、ThreadLocal内存泄漏问题
5.1 为什么会内存泄漏
先看 Entry 的结构:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // Value 是强引用
}
- Key(ThreadLocal):弱引用,GC 时会被回收
- Value:强引用,不会被自动回收
泄漏场景:
1. ThreadLocal 对象被设为 null
2. GC 回收 ThreadLocal 对象(因为是弱引用)
3. Entry 的 Key 变成 null,但 Value 还在
4. 如果线程一直存活(比如线程池),这个 Value 永远无法被回收
Thread
└── ThreadLocalMap
└── Entry
├── Key: null(已被 GC 回收)
└── Value: 还在!无法回收!← 内存泄漏
5.2 为什么 Key 要设计成弱引用
如果 Key 是强引用:
ThreadLocal tl = new ThreadLocal();
tl.set("value");
tl = null; // 想释放 ThreadLocal
// 但是 Entry 还持有 ThreadLocal 的强引用
// ThreadLocal 对象无法被回收 ← 更严重的内存泄漏!
设计成弱引用后,至少 ThreadLocal 对象可以被回收,只是 Value 还在。
5.3 ThreadLocal 的自我清理机制
ThreadLocal 在 get()、set()、remove() 时会顺便清理 Key 为 null 的 Entry:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
// 清理 Key 为 null 的 Entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
// ...
}
但这个清理是被动的,如果一直不调用这些方法,泄漏的 Value 就一直存在。
5.4 正确的使用姿势
原则:用完必须调用 remove()
public class UserContextHolder {
private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
public static void setUser(User user) {
userHolder.set(user);
}
public static User getUser() {
return userHolder.get();
}
// 关键:提供 clear 方法
public static void clear() {
userHolder.remove();
}
}
// 使用时
try {
UserContextHolder.setUser(user);
// 业务逻辑...
} finally {
UserContextHolder.clear(); // 必须清理!
}
在 Web 应用中:
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(...) {
UserContextHolder.setUser(user);
return true;
}
@Override
public void afterCompletion(...) {
UserContextHolder.clear(); // 请求结束时清理
}
}
六、实战最佳实践
6.1 完整的上下文工具类
public class RequestContext {
private static final ThreadLocal<Map<String, Object>> CONTEXT =
ThreadLocal.withInitial(HashMap::new);
// 设置属性
public static void set(String key, Object value) {
CONTEXT.get().put(key, value);
}
// 获取属性
@SuppressWarnings("unchecked")
public static <T> T get(String key) {
return (T) CONTEXT.get().get(key);
}
// 获取属性,带默认值
@SuppressWarnings("unchecked")
public static <T> T get(String key, T defaultValue) {
T value = (T) CONTEXT.get().get(key);
return value != null ? value : defaultValue;
}
// 移除属性
public static void remove(String key) {
CONTEXT.get().remove(key);
}
// 清空所有(重要!)
public static void clear() {
CONTEXT.remove();
}
// 常用快捷方法
public static void setUserId(Long userId) {
set("userId", userId);
}
public static Long getUserId() {
return get("userId");
}
public static void setTraceId(String traceId) {
set("traceId", traceId);
}
public static String getTraceId() {
return get("traceId");
}
}
6.2 配合 Spring 拦截器使用
@Component
public class ContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 生成 TraceId
String traceId = UUID.randomUUID().toString().replace("-", "");
RequestContext.setTraceId(traceId);
// 解析用户信息
String token = request.getHeader("Authorization");
if (StringUtils.hasText(token)) {
Long userId = tokenService.parseUserId(token);
RequestContext.setUserId(userId);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 请求结束,必须清理
RequestContext.clear();
}
}
6.3 线程池场景的注意事项
线程池中的线程是复用的,如果不清理 ThreadLocal,下一个任务会拿到上一个任务的数据。
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> {
try {
RequestContext.setUserId(100L);
// 业务逻辑...
} finally {
RequestContext.clear(); // 必须清理!
}
});
更优雅的方式:封装任务包装器
public class ContextAwareRunnable implements Runnable {
private final Runnable task;
private final Map<String, Object> context;
public ContextAwareRunnable(Runnable task) {
this.task = task;
// 捕获提交任务时的上下文
this.context = new HashMap<>(RequestContext.getAll());
}
@Override
public void run() {
try {
// 恢复上下文
RequestContext.setAll(context);
task.run();
} finally {
// 清理上下文
RequestContext.clear();
}
}
}
// 使用
executor.submit(new ContextAwareRunnable(() -> {
// 可以获取到父线程的上下文
Long userId = RequestContext.getUserId();
}));
七、InheritableThreadLocal
7.1 问题:子线程无法获取父线程的 ThreadLocal
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("父线程的值");
new Thread(() -> {
System.out.println(threadLocal.get()); // null!
}).start();
子线程无法获取父线程设置的值。
7.2 解决方案:InheritableThreadLocal
InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
inheritableThreadLocal.set("父线程的值");
new Thread(() -> {
System.out.println(inheritableThreadLocal.get()); // "父线程的值"
}).start();
原理:创建子线程时,会把父线程的 InheritableThreadLocal 值复制一份给子线程。
7.3 局限性
InheritableThreadLocal 只在创建线程时复制,对于线程池场景不适用:
ExecutorService executor = Executors.newFixedThreadPool(1);
inheritableThreadLocal.set("任务1的值");
executor.submit(() -> {
System.out.println(inheritableThreadLocal.get()); // "任务1的值" ✅
});
inheritableThreadLocal.set("任务2的值");
executor.submit(() -> {
System.out.println(inheritableThreadLocal.get()); // 还是"任务1的值" ❌
});
因为线程池复用线程,第二个任务用的是已经创建好的线程,不会重新复制。
解决方案:使用阿里开源的 TransmittableThreadLocal(TTL)。
八、总结
8.1 核心要点
| 要点 | 说明 |
|---|---|
| 适用场景 | 每个线程需要独立的上下文数据 |
| 不适用场景 | 多线程竞争修改同一个值 |
| 底层结构 | 每个 Thread 持有 ThreadLocalMap,Key 是 ThreadLocal |
| 内存泄漏 | Key 是弱引用会被回收,Value 是强引用不会自动回收 |
| 最佳实践 | 用完必须调用 remove() |
8.2 使用场景总结
| 场景 | 示例 |
|---|---|
| 请求上下文 | 用户信息、租户信息、TraceId |
| 数据库连接 | 同一事务内复用连接 |
| 日期格式化 | SimpleDateFormat 线程安全问题 |
| 分布式追踪 | 链路追踪 TraceId 传递 |
8.3 ThreadLocal vs 锁
| 对比项 | ThreadLocal | 锁(synchronized/Lock) |
|---|---|---|
| 解决思路 | 空间换时间,每个线程一份副本 | 时间换安全,排队访问 |
| 适用场景 | 线程隔离,各自操作各自的数据 | 线程同步,共同操作同一份数据 |
| 性能 | 无竞争,性能高 | 有竞争,性能相对低 |
| 数据一致性 | 不保证(各自独立) | 保证(同一份数据) |
记住:ThreadLocal 不是用来解决共享变量的线程安全问题的,而是用来实现线程隔离的。
热门专栏推荐
- Agent小册
- Java基础合集
- Python基础合集
- Go基础合集
- 大数据合集
- 前端小册
- 数据库合集
- Redis 合集
- Spring 全家桶
- 微服务全家桶
- 数据结构与算法合集
- 设计模式小册
- 消息队列合集
等等等还有许多优秀的合集在主页等着大家的光顾,感谢大家的支持
文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😊
希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🙏
如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🌟
更多推荐
所有评论(0)