java线程变量ThreadLocal用法篇v1.1
java线程变量ThreadLocal用法篇
介绍
ThreadLocal 是 Java 中用于实现 线程封闭 的类,其核心作用是为每个线程提供独立的变量副本,避免多线程间的共享竞争。但它不保证存储对象的线程安全,存储的对象使用不当仍会引发线程安全问题。线程安全的本质是 “对象不被共享” 或 “对象不可变”,而非依赖 ThreadLocal,在分布式线程环境(如线程池、异步任务)中,必须配合 remove() 清理数据或者使用阿里巴巴的TransmittableThreadLocal ,否则会引发数据污染和内存泄漏。
核心作用
1、线程隔离
每个线程通过 ThreadLocal 获取到的是独立的对象副本,线程间互不干扰。
static final ThreadLocal<User> userContext = new ThreadLocal<>();// 线程A
userContext.set(new User("Alice")); // 线程A的副本// 线程B
userContext.set(new User("Bob")); // 线程B的副本
2、可以避免传参
在调用链中隐式传递上下文(如用户身份、事务ID),无需显式传递参数。
线程安全问题 InheritableThreadLocal
1、线程隔离机制分析
protected static final ThreadLocal<BusinessHandler> BUSINESS_HOLDER = new InheritableThreadLocal<>() {@Overrideprotected BusinessHandler initialValue() {return new BusinessHandler();}};
InheritableThreadLocal 确保每个线程有自己的 BusinessHandler 实例(通过 initialValue() 初始化)
对象不跨线程共享:不同线程操作不同实例,无并发冲突,但子线程会复制父线程的值(如果用的普通 ThreadLocal 子线程获取的是 null),若父子线程同时修改 BusinessHandler,可能引发数据错乱。
public static void main(String[] args) {BusinessHandler instance = BusinessHandler.getInstance();StructureDto structureDto = new StructureDto();instance.getMap().put("a", structureDto);// 创建子线程(自动复制父线程的TradeCacheHandler)Thread childThread = new Thread(() -> {// 子线程操作的是父线程的同一个实例!Map<String, StructureDto> dataMap =BusinessHandler.getInstance().getMap();if(dataMap.containsKey("a")){// 可以输出System.out.println("子线程操作的是父线程的同一个实例!");}});childThread.start();}
修复父子线程共享问题
重写 childValue 实现深拷贝,将父线程的数据按需设置到子线程中。
protected static final ThreadLocal<BusinessHandler> BUSINESS_HOLDER = new InheritableThreadLocal<>() {@Overrideprotected BusinessHandler initialValue() {return new BusinessHandler();}@Overrideprotected TradeCacheHandler childValue(BusinessHandler parentValue) {// 创建新实例而非共享父线程对象,可以将parentValue需要的数据传到子线程的对象中,属于深拷贝return new BusinessHandler();}
};
除非必须继承父线程上下文,否则优先用普通 ThreadLocal(InheritableThreadLocal 是主要风险源)
3、线程池污染分析
线程池中处理完数据,没有清理ThreadLocal,导致下个任务使用同一个线程执行时,可以拿到前一个任务的残留数据
executor.execute(() -> {BUSINESS_HOLDER.get().processTrade();// 未清理!线程重用导致后续任务读到旧数据
});// 下一个任务在同一线程执行
executor.execute(() -> {// 可能拿到前一个任务的残留数据!BUSINESS_HOLDER dirtyHandler = BUSINESS_HOLDER.get();
});
强制线程任务结束清理
executor.execute(() -> {try {BUSINESS_HOLDER.get().doWork();} finally {BUSINESS_HOLDER.remove(); // 必须清理!}
});
或使用阿里巴巴 的TransmittableThreadLocal ,TransmittableThreadLocal (TTL)和ThreadLocal一样拥有基本的线程隔离功能及在父子线程共享上 和InheritableThreadLocal一样(除非进行的深拷贝)共享相同的引用内容,如果非引用对象使用TTL可以做到精准的隔离(数据为非引用对象才行,引用对象还是无法隔离需要做深拷贝才行),TTL对非引用数据做到精准隔离关键在于在任务被线程池中的线程执行前后,动态地设置和清理线程本地变量,从而避免了线程复用导致的上下文污染
// 初始化非引用数据的TTL第一次任务修改了数据,在第二次任务即使是相同线程依然是初始化的数据,不会受第一次任务的影响。
TransmittableThreadLocal<String> ttlContext = new TransmittableThreadLocal<>();
ExecutorService executor = Executors.newSingleThreadExecutor();// 线程原始状态
ttlContext.set("原始数据");// 提交任务
executor.submit(TtlRunnable.get(() -> {// 1. 执行前:自动设置任务上下文ttlContext.set("任务数据");// 2. 执行业务逻辑...processBusiness(ttlContext.get());// 3. 执行后:自动恢复线程原始状态
}));// 任务执行完毕后,线程的 ttlContext 自动恢复为"原始数据"
虽然 TransmittableThreadLocal 提供了强大的上下文传递能力,并能在任务执行后恢复线程原有上下文-4,但主动调用 remove() 是一个能有效避免内存泄漏和数据混乱的好习惯。请记住这个关键原则:谁设置,谁清理;用完即清理。
Maven 依赖:
<dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId><version>2.14.2</version>
</dependency>
4、对象逃逸分析
BusinessHandler businessHandler = getHandler();
public BusinessHandler getHandler() {// 危险!暴露内部引用给其他线程return BUSINESS_HOLDER.get();
}// businessHandler 传递到其他线程拿到引用后非法修改
new Thread(() -> {businessHandler.getHandler().clear(); // 并发破坏!
}).start();
解决方法,不提供返回对象应用,只提供操作方法。这样可以防止对象传到其他线程去
ThreadLocal原理及内存泄漏分析
1、介绍
每个线程内部都有一个私有的 ThreadLocalMap 实例,用于存储该线程关联的所有 ThreadLocal 变量。 ThreadLocalMap 内部的 Entry 继承自 WeakReference<ThreadLocal<?>>。所以**Entry 的 key(即 ThreadLocal 对象本身)是通过弱引用指向的**。
Entry 中的 value 字段始终保持着对实际存储值的强引用。
2、内存泄漏问题分析
当一个 ThreadLocal 对象(也就是key)在应用程序中不再被任何强引用指向时(例如,声明它的类实例被回收,或者局部变量超出作用域),在下一次垃圾回收(GC)时,这个弱引用 会被回收即GC 会回收这个 ThreadLocal 对象,Entry 中的 key 就会变为 null。但是value被 Entry 强引用着,只要线程不死,当前线程ThreadLocalMap的Entry一直强引用者value。假设是长期存活的线程池中的线程,这种泄漏可能会不断累积,最终可能导致 内存溢出OutOfMemoryError。
3、防止内存泄漏方法:
- 在使用完毕后调用
ThreadLocal.remove(),remove()放在finally块中执行,确保无论代码逻辑是否发生异常都能清理资源。 - 将 ThreadLocal 定义为
static final,这本身不会导致泄漏(因为ThreadLocal强引用在类上),但用完之后必须主动remove,否则可能将数据带到下个线程去。
