ThreadLocal 核心概念
「一个变量,让每个线程都觉得自己是独享的。」
这不是什么魔法,而是 ThreadLocal 的核心能力。
在 Web 开发中,你一定见过这样的代码:
java
// 在 Controller 设置用户信息
currentUser.set(user);
// 在 Service 层获取
User user = currentUser.get();
// 在 DAO 层还能获取到同一个用户
User user = currentUser.get(); // 依然是同一个整个调用链没有任何显式的参数传递,但每层都能拿到当前请求的用户信息。这就是 ThreadLocal 的威力。
为什么需要 ThreadLocal
多线程环境下,传统的数据传递方式有两种:
方式一:方法参数传递
java
void controller(User user) {
service.process(user);
}
void service(User user) {
dao.save(user);
}
void dao(User user) {
// 每层都要接收和传递
}缺点:每个方法都要加参数,层层穿透,代码臃肿。
方式二:共享变量
java
private static User currentUser; // 所有线程共享
void controller() {
currentUser = getUserFromRequest();
service.process(); // 不传参了
}缺点:线程 A 设置的用户,被线程 B 读取到了,数据混乱。
ThreadLocal 的出现,就是为了解决这个两难问题。
ThreadLocal 的本质
ThreadLocal 为每个线程提供独立的变量副本:
┌──────────────────────────────────────────────────────────┐
│ ThreadLocal 工作原理 │
├──────────────────────────────────────────────────────────┤
│ │
│ ThreadLocal Thread │
│ ┌────────────┐ ┌──────────────────────────┐ │
│ │ value │◄────►│ ThreadLocalMap │ │
│ └────────────┘ │ ┌────────────────────┐ │ │
│ │ │ key → ThreadLocal │ │ │
│ │ │ value → 线程独享值 │ │ │
│ │ └────────────────────┘ │ │
│ └──────────────────────────┘ │
│ │
│ 核心:值存储在 Thread 对象中,ThreadLocal 只是访问入口 │
│ │
└──────────────────────────────────────────────────────────┘关键点:ThreadLocal 本身不存储值,它只是一个访问入口。真正的值存储在每个线程自己的 ThreadLocalMap 中。
基本用法
java
public class ThreadLocalBasicDemo {
private static final ThreadLocal<String> context = new ThreadLocal<>();
public static void main(String[] args) {
// 设置值
context.set("主线程的数据");
System.out.println("主线程: " + context.get());
// 新线程看不到主线程的值
Thread child = new Thread(() -> {
System.out.println("子线程读取: " + context.get()); // null
context.set("子线程的数据");
System.out.println("子线程设置后: " + context.get());
});
child.start();
child.join();
// 主线程的值不受影响
System.out.println("主线程最终: " + context.get());
}
}输出:
主线程: 主线程的数据
子线程读取: null
子线程设置后: 子线程的数据
主线程最终: 主线程的数据两个线程互不干扰,各自有各自的副本。
带初始值的 ThreadLocal
每次 get() 都判断 null 很麻烦?可以设置初始值:
java
public class ThreadLocalWithInitial {
// 方式1:匿名内部类重写 initialValue()
private static final ThreadLocal<List<String>> listLocal =
new ThreadLocal<List<String>>() {
@Override
protected List<String> initialValue() {
return new ArrayList<>();
}
};
// 方式2:withInitial() (JDK 8+,更简洁)
private static final ThreadLocal<Integer> intLocal =
ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
// 无需手动 set,直接 get 就有初始值
System.out.println(listLocal.get()); // []
System.out.println(intLocal.get()); // 0
// 使用
listLocal.get().add("item");
System.out.println(listLocal.get()); // [item]
// 新线程依然是初始值
new Thread(() ->
System.out.println("新线程 listLocal: " + listLocal.get()) // []
).start();
}
}ThreadLocal vs 共享变量
| 特性 | 普通静态变量 | ThreadLocal |
|---|---|---|
| 访问 | 所有线程共享 | 线程独享 |
| 同步需求 | 需要 synchronized | 不需要 |
| 生命周期 | 对象生命周期 | 线程生命周期 |
| 典型用途 | 共享数据 | 线程上下文 |
典型使用场景
场景一:用户上下文传递
java
public class UserContextHolder {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
public static void setUser(User user) {
currentUser.set(user);
}
public static User getUser() {
return currentUser.get();
}
public static void clear() {
currentUser.remove();
}
}
// Controller
public void handleRequest(HttpServletRequest request) {
User user = authenticate(request);
UserContextHolder.setUser(user); // 设置
try {
doSomething();
} finally {
UserContextHolder.clear(); // 清理
}
}
// Service(无需传参)
public void doSomething() {
User user = UserContextHolder.getUser(); // 直接获取
// ...
}场景二:日期格式化(线程安全)
SimpleDateFormat 不是线程安全的,传统做法是每次调用都 new 一个。ThreadLocal 可以让它被复用:
java
public class DateFormatter {
private static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() ->
new SimpleDateFormat("yyyy-MM-dd"));
public static String format(Date date) {
return dateFormat.get().format(date);
}
}每个线程用自己的 SimpleDateFormat,既线程安全,又避免了重复创建。
场景三:数据库连接(事务管理)
java
public class TransactionManager {
private static final ThreadLocal<Connection> connection =
new ThreadLocal<>();
public static Connection getConnection() {
Connection conn = connection.get();
if (conn == null) {
conn = DataSource.getConnection();
connection.set(conn);
}
return conn;
}
public static void beginTransaction() throws SQLException {
getConnection().setAutoCommit(false);
}
public static void commit() throws SQLException {
getConnection().commit();
}
public static void rollback() throws SQLException {
getConnection().rollback();
}
public static void close() {
connection.remove();
}
}Spring 中的 ThreadLocal
Spring 大量使用 ThreadLocal 来传递上下文:
RequestContextHolder:存储当前 HTTP 请求TransactionSynchronizationManager:管理事务上下文SecurityContextHolder:存储安全认证信息
理解 ThreadLocal,是理解 Spring 框架内部机制的基础。
注意事项
ThreadLocal 不是万能钥匙,有些坑需要避开:
- 必须清理:使用完毕后调用
remove(),否则可能导致内存泄漏 - 子线程不继承:普通 ThreadLocal,子线程无法访问父线程的值(需要用 InheritableThreadLocal)
- 线程池问题:线程池复用的线程,如果上次的值没清理,会被下个任务看到
- 不是线程同步:ThreadLocal 不能替代 synchronized,它解决的是隔离问题,不是同步问题
要点回顾
- ThreadLocal 为每个线程提供独立的变量副本
- 值存储在
Thread.threadLocals中,不是存储在 ThreadLocal 本身 - 使用
get()/set()/remove()操作 - 使用
withInitial()或重写initialValue()设置初始值 - 用完必须 remove(),特别是线程池场景
相关链接
- InheritableThreadLocal - 子线程继承父线程的值
- ThreadLocal 实现原理 - 深入理解内部机制
- ThreadLocal 内存泄漏 - 避免常见的内存陷阱
- ThreadLocal 最佳实践 - 工程实践指南
