基础概念
什么是ThreadLocal
ThreadLocal 是 Java 中一个特殊的类,它为每个使用该变量的线程提供独立的变量副本,实现了线程间的数据隔离
核心特点:
- 线程隔离:每个线程访问的是自己的变量副本,互不干扰
- 避免同步:由于数据不共享,无需同步操作
- 内存泄漏风险:使用不当可能导致内存泄漏
工作原理
ThreadLocal 内部使用 ThreadLocalMap 存储数据:
- 每个 Thread 对象内部都有一个 ThreadLocalMap
- ThreadLocal 作为 Map 的 key
- set()/get() 操作实际是在当前线程的 ThreadLocalMap 上工作
源码如下:
private void set(Thread t, T value) {
ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap
if (map != null) {
map.set(this, value);
} else {
createMap(t, value); // 首次使用创建Map
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; // 直接返回线程的成员变量
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
public T get() {
return get(Thread.currentThread());
}
private T get(Thread t) {
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
// 返回初始值,默认是null,也可以通过重写 initialValue()方法为ThreadLocal 变量提供初始值
return setInitialValue(t);
}
ThreadLocal 高级特性
InheritableThreadLocal
InheritableThreadLocal 是 ThreadLocal 的子类,它允许子线程继承父线程的线程局部变量值
特点:
- 当创建新线程时,新线程会继承父线程的所有可继承线程局部变量
- 适用于需要在父子线程间传递上下文信息的场景
public class UserContext {
private static final InheritableThreadLocal<User> currentUser =
new InheritableThreadLocal<>();
public static void setCurrentUser(User user) {
currentUser.set(user);
}
public static User getCurrentUser() {
return currentUser.get();
}
public static void clear() {
currentUser.remove();
}
}
// 业务使用示例
public class ReportGenerationService {
public void generateReportAsync(User user, ReportRequest request) {
UserContext.setCurrentUser(user);
try {
// 主线程准备数据
prepareData(request);
// 异步生成报告(可能耗时)
new Thread(() -> {
try {
// 子线程可以获取用户信息
User currentUser = UserContext.getCurrentUser();
if (!currentUser.hasPermission("report.generate")) {
throw new SecurityException("无权限生成报告");
}
// 生成报告逻辑...
generatePDFReport(request, currentUser);
} finally {
UserContext.clear();
}
}).start();
} finally {
UserContext.clear();
}
}
}
ThreadLocal 与内存泄漏
- ThreadLocal 的键是弱引用,但值是强引用
- 如果线程长时间运行且不调用 remove(),可能导致值无法被回收
强引用和弱引用:
引用类型决定了对象如何被垃圾回收器(GC)处理,不同引用类型对内存管理有重要影响
强引用:
- 默认的引用类型:普通对象赋值(如
Object obj = new Object())就是强引用- GC行为:只要强引用存在,对象就不会被回收,即使内存不足时抛出
OutOfMemoryError- 生命周期:对象的生命周期由强引用决定,手动置为
null或超出作用域后才会成为垃圾回收候选Object strongRef = new Object(); // 强引用 strongRef = null; // 手动断开强引用,此时对象可被GC回收弱引用:
- 通过
WeakReference类实现:需要显式使用java.lang.ref.WeakReference- GC行为:当只有弱引用指向对象时,无论内存是否充足,GC都会回收该对象
- 生命周期:对象存活到下一次GC运行
Object obj = new Object(); WeakReference<Object> weakRef = new WeakReference<>(obj); // 创建弱引用 obj = null; // 断开强引用 System.gc(); // 触发GC(仅示例,不保证立即执行) // 弱引用的对象可能已被回收 System.out.println(weakRef.get()); // 可能输出 null此外还有软引用和虚引用,这里不做多介绍
ThreadLocal与线程池
值污染
值污染发生在同一个线程被不同任务复用时,前一个任务设置的ThreadLocal值未被清理,被后一个任务意外读取
final ThreadLocal<Integer> taskIdHolder = new ThreadLocal<>();
ExecutorService pool = Executors.newSingleThreadExecutor(); // 单线程池
// 任务1:设置值但忘记清理
pool.execute(() -> {
taskIdHolder.set(1);
System.out.println("任务1设置: " + taskIdHolder.get());
});
// 任务2:未设置值却读取到残留值
pool.execute(() -> {
System.out.println("任务2读取: " + taskIdHolder.get()); // 污染发生!
});
// 任务3:正确清理模式
pool.execute(() -> {
try {
taskIdHolder.set(3);
System.out.println("任务3设置: " + taskIdHolder.get());
} finally {
taskIdHolder.remove();
}
});
// 任务4:验证清理效果
pool.execute(() -> {
System.out.println("任务4读取: " + taskIdHolder.get()); // 输出null
});