ThreadLocal 方法详解
get()、set()、remove()……ThreadLocal 的方法不多,但每个都有门道。
用错轻则拿不到值,重则内存泄漏。本文逐个拆解。
五个核心方法
| 方法 | 说明 | 返回值 |
|---|---|---|
get() | 获取当前线程的值 | T(无则返回初始值或 null) |
set(T value) | 设置当前线程的值 | void |
remove() | 移除当前线程的值 | void |
initialValue() | 提供初始值(受保护方法) | T |
withInitial(Supplier) | JDK 8+,创建带初始值的实例 | ThreadLocal<T> |
get() 方法
返回当前线程存储的值。如果从未设置过,返回初始值(由 initialValue() 提供)。
java
public class GetMethodDemo {
private static final ThreadLocal<String> context = new ThreadLocal<>();
public static void main(String[] args) {
// 未设置 → 返回 null(无初始值)
System.out.println(context.get()); // null
// 设置后 → 返回设置的值
context.set("Hello");
System.out.println(context.get()); // Hello
// 有初始值时
ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> 0);
System.out.println(counter.get()); // 0
}
}get() 的内部流程:
get() 被调用
│
├─► 获取当前线程 t
│
├─► 获取 t.threadLocals(ThreadLocalMap)
│
├─► Map 非空 → 根据当前 ThreadLocal 查找 Entry
│ └─► 找到 → 返回 value
│ └─► 找不到 → 调用 setInitialValue()
│
└─► setInitialValue()
├─► 调用 initialValue() 获取初始值
├─► 创建 ThreadLocalMap(如不存在)
└─► 存入初始值并返回set(T value) 方法
设置当前线程的值。如果之前有值,会被覆盖。
java
public class SetMethodDemo {
private static final ThreadLocal<Integer> counter = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
// 每个线程独立计数
Thread t1 = new Thread(() -> {
counter.set(10);
System.out.println("Thread-1 第一次: " + counter.get()); // 10
counter.set(counter.get() + 1);
System.out.println("Thread-1 第二次: " + counter.get()); // 11
});
Thread t2 = new Thread(() -> {
// t2 从来没 set 过 → 返回初始值 null
System.out.println("Thread-2: " + counter.get()); // null
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}remove() 方法
删除当前线程存储的值。删除后,再次调用 get() 会触发 initialValue() 或返回 null。
这是最重要但最容易被遗忘的方法。
java
public class RemoveMethodDemo {
private static final ThreadLocal<String> data = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
data.set("Value");
System.out.println("设置后: " + data.get()); // Value
data.remove();
System.out.println("删除后: " + data.get()); // null
// 线程池场景:必须清理
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 3; i++) {
final int taskId = i;
executor.submit(() -> {
try {
data.set("Task-" + taskId);
System.out.println(Thread.currentThread().getName() +
" 设置: " + data.get());
Thread.sleep(100);
// ⚠️ 不清理的话,同一个线程的下次任务会看到上次残留的值
System.out.println(Thread.currentThread().getName() +
" 处理后: " + data.get());
} finally {
data.remove(); // ✅ 必须在 finally 中清理
}
});
}
executor.shutdown();
executor.awaitTermination(3, TimeUnit.SECONDS);
}
}输出示例(注意线程复用问题):
pool-1-thread-1 设置: Task-0
pool-1-thread-2 设置: Task-1
pool-1-thread-1 处理后: Task-0
pool-1-thread-2 处理后: Task-1
pool-1-thread-1 设置: Task-2
pool-1-thread-1 处理后: Task-2如果去掉 remove(),第三次 pool-1-thread-1 会看到 Task-0 的残留值。
initialValue() 方法
protected 方法,子类重写以提供初始值。每次 get() 时,如果从未 set() 过,就会调用此方法。
java
public class InitialValueDemo {
// 匿名内部类方式
private static final ThreadLocal<List<String>> listLocal =
new ThreadLocal<List<String>>() {
@Override
protected List<String> initialValue() {
return new ArrayList<>();
}
};
// 使用泛型方法(JDK 8+ 推荐)
private static final ThreadLocal<List<String>> listLocal2 =
ThreadLocal.withInitial(ArrayList::new);
public static void main(String[] args) {
// 直接 get 就有值,无需先 set
System.out.println(listLocal.get()); // []
listLocal.get().add("item");
System.out.println(listLocal.get()); // [item]
// 新线程同样是初始值
new Thread(() ->
System.out.println("新线程: " + listLocal.get()) // []
).start();
}
}withInitial(Supplier) 方法
JDK 8 引入的工厂方法,更简洁地创建带初始值的 ThreadLocal:
java
public class WithInitialDemo {
public static void main(String[] args) {
// String
ThreadLocal<String> stringLocal =
ThreadLocal.withInitial(() -> "default");
// Integer
ThreadLocal<Integer> intLocal =
ThreadLocal.withInitial(() -> 0);
// 对象
ThreadLocal<SimpleDateFormat> dateFormatLocal =
ThreadLocal.withInitial(() ->
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
// 集合
ThreadLocal<List<String>> listLocal =
ThreadLocal.withInitial(ArrayList::new);
// Map
ThreadLocal<Map<String, Object>> mapLocal =
ThreadLocal.withInitial(LinkedHashMap::new);
System.out.println(stringLocal.get());
System.out.println(intLocal.get());
System.out.println(dateFormatLocal.get().format(new Date()));
System.out.println(listLocal.get());
}
}综合使用示例
完整的请求上下文管理:
java
public class RequestContext {
private static final ThreadLocal<UserContext> userContext =
new ThreadLocal<>();
private static final ThreadLocal<RequestId> requestId =
ThreadLocal.withInitial(RequestId::new);
public static void main(String[] args) throws InterruptedException {
processRequest("user1", "req-001");
processRequest("user2", "req-002");
}
private static void processRequest(String username, String reqId) {
try {
userContext.set(new UserContext(username));
requestId.get().setId(reqId);
System.out.println("开始处理请求: " + requestId.get());
callService();
callDao();
} finally {
// ⚠️ 重要:每次请求结束必须清理
userContext.remove();
requestId.remove();
}
}
private static void callService() {
System.out.println(" Service: " + userContext.get() +
", " + requestId.get());
}
private static void callDao() {
System.out.println(" DAO: " + userContext.get() +
", " + requestId.get());
}
static class UserContext {
String username;
UserContext(String username) { this.username = username; }
@Override
public String toString() { return "User[" + username + "]"; }
}
static class RequestId {
String id;
void setId(String id) { this.id = id; }
@Override
public String toString() { return "RequestId[" + id + "]"; }
}
}方法对比一览
| 方法 | 调用时机 | 线程安全 | 注意事项 |
|---|---|---|---|
get() | 读取 | 安全 | 无值时调用 initialValue |
set() | 写入 | 安全 | 覆盖已有值 |
remove() | 删除 | 安全 | 最重要,容易遗漏 |
initialValue() | get 时触发 | 安全 | 仅在首次 get 时调用 |
withInitial() | 创建时 | 安全 | JDK 8+ 推荐方式 |
常见错误
错误一:忘记清理
java
// ❌ 错误
void process() {
context.set(value);
doSomething();
// 线程复用后,下一个任务会看到残留值
}
// ✅ 正确
void process() {
try {
context.set(value);
doSomething();
} finally {
context.remove();
}
}错误二:以为子线程能看到父线程的值
java
// ❌ 错误认知
context.set("父线程");
new Thread(() -> {
context.get(); // 返回 null,不是 "父线程"
});如果需要子线程继承,使用 InheritableThreadLocal。
要点回顾
get():返回当前线程的值,无值则返回初始值set():设置当前线程的值,会覆盖之前的值remove():最重要,删除当前线程的值,用完必须调用initialValue():提供初始值,仅在首次 get 时调用withInitial():JDK 8+ 的工厂方法,更简洁
最佳实践:始终使用 withInitial() 或重写 initialValue() 提供初始值,减少 null 判断;在 try-finally 中确保 remove() 一定被调用。
相关链接
- ThreadLocal 核心概念 - 基本原理
- InheritableThreadLocal - 子线程继承
- ThreadLocal 内存泄漏 - remove() 的重要性
