ThreadLocal 原理与实战
ThreadLocal 实现线程隔离的数据存储,是用户上下文、数据库连接等场景的核心技术。
一、核心概念
1.1 什么是 ThreadLocal
ThreadLocal = 线程本地变量
每个线程都有自己独立的变量副本
↓
线程间数据隔离,互不干扰
↓
无需同步,提高性能
1.2 典型应用场景
1. 用户上下文(UserContext)
2. 数据库连接(Connection)
3. Session 管理
4. 事务管理
5. TraceId 追踪
二、基本使用
2.1 创建 ThreadLocal
// 创建 ThreadLocal
ThreadLocal<String> threadLocal = new ThreadLocal<>();
// 设置值
threadLocal.set("Hello");
// 获取值
String value = threadLocal.get();
// 移除值
threadLocal.remove();
2.2 线程隔离示例
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
// 线程 A
new Thread(() -> {
threadLocal.set(1);
System.out.println(threadLocal.get()); // 1
}).start();
// 线程 B
new Thread(() -> {
threadLocal.set(2);
System.out.println(threadLocal.get()); // 2
}).start();
// 主线程
System.out.println(threadLocal.get()); // null
2.3 初始值
ThreadLocal<SimpleDateFormat> threadLocal =
new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
// 使用
String date = threadLocal.get().format(new Date());
三、实现原理
3.1 核心结构
graph TB
A[Thread] --> B[ThreadLocalMap]
B --> C[Entry 1]
B --> D[Entry 2]
C --> E[ThreadLocal 引用]
C --> F[Value]
3.2 源码分析
// Thread 类中
ThreadLocal.ThreadLocalMap threadLocals;
// ThreadLocalMap 结构
static class ThreadLocalMap {
// Entry 数组
private Entry[] table;
// Entry 继承 WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 弱引用 ThreadLocal
value = v;
}
}
}
// set() 方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// get() 方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
3.3 哈希冲突解决
// 线性探测法
// 如果 hash 位置被占用,向后查找空位
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 线性探测
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.value = value;
return;
}
}
// 找到空位,插入
tab[i] = new Entry(key, value);
}
四、内存泄漏问题
4.1 为什么使用弱引用
ThreadLocalMap 的 Entry 继承 WeakReference
↓
key 是 ThreadLocal 的弱引用
↓
ThreadLocal 对象可被 GC 回收
4.2 内存泄漏场景
// ❌ 内存泄漏
ThreadLocal<User> userLocal = new ThreadLocal<>();
// 线程池场景
executor.submit(() -> {
userLocal.set(currentUser); // 设置用户
doWork();
// 忘记 remove,线程复用导致内存泄漏
});
泄漏原因:
- 线程池线程长期存活
- ThreadLocalMap 持有 value 强引用
- ThreadLocal 被回收,但 value 无法回收
4.3 解决方案
// ✅ 正确做法:finally 中 remove
ThreadLocal<User> userLocal = new ThreadLocal<>();
executor.submit(() -> {
try {
userLocal.set(currentUser);
doWork();
} finally {
userLocal.remove(); // 必须清理
}
});
4.4 InheritableThreadLocal 泄漏
// InheritableThreadLocal 父子线程共享
InheritableThreadLocal<String> local = new InheritableThreadLocal<>();
// 主线程设置
local.set("parent");
// 子线程继承
new Thread(() -> {
System.out.println(local.get()); // "parent"
}).start();
// 问题:子线程修改不影响父线程
// 解决:使用 ChildInheritableThreadLocal 或手动传递
五、实战场景
5.1 用户上下文
public class UserContext {
private static final ThreadLocal<User> userLocal =
ThreadLocal.withInitial(() -> null);
public static void setUser(User user) {
userLocal.set(user);
}
public static User getUser() {
return userLocal.get();
}
public static void clear() {
userLocal.remove();
}
}
// 使用
public class UserService {
public void process() {
try {
UserContext.setUser(currentUser);
// 业务逻辑
} finally {
UserContext.clear();
}
}
}
5.2 数据库连接管理
public class DBConnection {
private static final ThreadLocal<Connection> connectionLocal =
ThreadLocal.withInitial(() -> {
try {
return dataSource.getConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
});
public static Connection getConnection() {
return connectionLocal.get();
}
public static void closeConnection() {
Connection conn = connectionLocal.get();
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
connectionLocal.remove();
}
}
5.3 事务管理
public class TransactionManager {
private static final ThreadLocal<Boolean> transactionLocal =
ThreadLocal.withInitial(() -> false);
public void begin() {
transactionLocal.set(true);
DBConnection.getConnection().setAutoCommit(false);
}
public void commit() {
try {
DBConnection.getConnection().commit();
} finally {
transactionLocal.remove();
}
}
public void rollback() {
try {
DBConnection.getConnection().rollback();
} finally {
transactionLocal.remove();
}
}
public boolean isInTransaction() {
return transactionLocal.get();
}
}
5.4 TraceId 追踪
public class TraceContext {
private static final ThreadLocal<String> traceLocal =
ThreadLocal.withInitial(() -> UUID.randomUUID().toString());
public static String getTraceId() {
return traceLocal.get();
}
public static void setTraceId(String traceId) {
traceLocal.set(traceId);
}
public static void clear() {
traceLocal.remove();
}
}
// 使用
@Slf4j
public class Service {
public void process() {
try {
TraceContext.setTraceId(generateTraceId());
log.info("Processing with traceId: {}", TraceContext.getTraceId());
} finally {
TraceContext.clear();
}
}
}
六、最佳实践
6.1 必须清理
// ❌ 错误:不清理
threadLocal.set(value);
doWork();
// 内存泄漏
// ✅ 正确:finally 清理
try {
threadLocal.set(value);
doWork();
} finally {
threadLocal.remove();
}
6.2 线程池场景
// 线程池必须清理
executor.submit(() -> {
try {
context.set(ctx);
return doWork();
} finally {
context.remove();
}
});
6.3 初始值优化
// 使用 withInitial 代替匿名类
ThreadLocal<SimpleDateFormat> local =
ThreadLocal.withInitial(SimpleDateFormat::new);
// 优于
ThreadLocal<SimpleDateFormat> local =
new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat();
}
};
6.4 避免过度使用
// ❌ 不推荐:方法参数传递更清晰
public void process(User user) {
doSomething(user);
}
// ✅ 推荐:跨多层调用
public void process() {
UserContext.setUser(user);
service.doSomething(); // 深层调用可获取 user
}
七、总结
ThreadLocal 核心要点:
| 特性 | 说明 |
|---|---|
| 线程隔离 | 每个线程独立副本 |
| 实现原理 | ThreadLocalMap + Entry |
| 弱引用 | key 是弱引用,防止泄漏 |
| 内存泄漏 | 线程池场景必须 remove |
| 应用场景 | 用户上下文、数据库连接、TraceId |
ThreadLocal 是线程隔离的利器,但必须注意内存泄漏问题,finally 中务必 remove。