01 引言
Spring MVC
有父子容器,Java
的线程也有父子线程。父子线上会像父子容器一样,子容器可以使用父容器的资源么?父子线程又会上演怎样的传承关系呢?
02 父子线程
父子线程的表现形式更加简单,就是线程的嵌套。最外层的为父线程,嵌套在内部的线程为子线程。线程上下文的变量是ThreadLocal
, 而ThreadLocal
在线程之间是不可共享,我们称之为每个线程的一个副本。父子线程之间同样不可共享。
2.1 父子线程案例
案例的父线程就是主线程,而子线程就是new Thread()
线程。
ThreadLocal<String> local = new ThreadLocal<>();
local.set("test");
for (int i = 0; i <3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "获取数据:local=" + local.get());
}, "线程【" + i + "】").start();
}
System.out.println(Thread.currentThread().getName() + "获取数据:local=" + local.get());
执行结果
2.2 案例分析
案例中在主线线程中设置了ThreadLocal
的值为test
,其子线程都没有获取到主线程的设置的上下文的值。因为ThreadLocal
是在每一个线程都会创建独属于自己的副本,且之间互不影响,保证了线程之间的数据安全。
从案例可以看出,子线程并不能像父子容器那样,获取父线程的上下文资源。父子线程之间互不干扰。
2.3 父子线程的传承
但是我们经常会有这样的场景,父线程处理了公用的数据,并保存在上文的线程变量中,子线程分批处理任务,处理任务时需要用到父线程的公用参数。这时,因为现成之间的隔离关系,就会导致参数无法传递。
这个时候,我们今天的主角闪亮登场:java.lang.InheritableThreadLocal
,InheritableThreadLocal
是ThreadLocal
的子类,专门处理父子继承关系的线程变量。
使用InheritableThreadLocal
改造一下案例代码:
从代码来看,只需要更换ThreadLocal
的子类,就可以实现线程上下文的传递。
2.4 子线程修改上下文变量
父线程的线程上下文会传递给子线程,如果子线程修改上下文变量后,父线程和其他子线程会影响么?根据ThreadLocal
本身的特性,每一个线程都是保存属于自己的副本,理论上应该是不会书影响的。
结果也确实如此。
2.5 源码解析
InheritableThreadLocal
的代码很简单:
核心就是inheritableThreadLocals
属性,而inheritableThreadLocals
会在创建线程的时候被赋值:
条件是通过创建线程的时候带过来的,默认就是true
03 父子线程的特性
线程之间互不影响,父子线程之间同样不受影响。
- 独立生命周期:父线程结束不影响子线程运行
- 环境继承:子线程默认继承父线程上下文
- 异常隔离:子线程异常不会直接影响父线程
- 资源竞争:共享资源需通过同步机制保护
- 上下文变动影响范围:父容器的上下文被子线程继承会影响到子线程,而子线程的修改不会影响父线程以及相邻线程
04 避坑指南
线程的上下文的生命周期跟随线程的生命周期,在使用线程池时,由于核心线程时不会销毁的,所以线程的上下文就会一直存在,更容易踩坑。
4.1 内存泄露陷阱
正如上面描述的,使用线程池时,核心线程不会销毁,如果线程的上下文占用内存比较大,就可能引起内存泄露的问题
// 错误示例:线程池未清理ThreadLocal
ExecutorService pool = Executors.newFixedThreadPool(5);
pool.execute(() -> {
inheritableThreadLocal.set(largeObject); // 大对象驻留内存
});
解决方案:
确保任务执行完毕之后,手动清理上下文的内容
// 解决方案:任务结束后必须remove
pool.execute(() -> {
try {
inheritableThreadLocal.set(largeObject);
// ...业务逻辑...
} finally {
inheritableThreadLocal.remove(); // 强制清理
}
});
4.2 线程饥饿死锁
父线程等待子线程的返回结果时,如果子线程提交的任务发生阻塞,则父线程同样会一直阻塞导致线程死锁。
// 父线程等待子线程结果
Future<String> future = executorService.submit(() -> {
// 子线程内又提交新任务(可能阻塞)
Future<Integer> innerFuture = executorService.submit(/*...*/);
return innerFuture.get() + " result";
});
// 当线程池满时,所有线程都在等待结果,导致死锁
String result = future.get();
解决方案:使用不同线程池处理不同层级任务
4.3 上下文的污染
线程池处理完任务之后,核心线程不会销毁,上文参数也会保留。如果新的任务进来,就可能获取到上一次任务的上下文内容。
// 线程池复用导致ThreadLocal污染
ExecutorService pool = Executors.newSingleThreadExecutor();
pool.execute(() -> {
inheritableThreadLocal.set("用户A数据");
});
// 后续任务可能读取到错误数据
pool.execute(() -> {
System.out.println(inheritableThreadLocal.get()); // 可能输出"用户A数据"
});
05 小结
父子线程如同程序世界的家族树,合理的继承机制能提升效率,而不当使用则会导致资源混乱。掌握线程间的"血缘关系",方能构建出高效稳健的并发系统。
多线程编程的艺术在于:让每个线程各司其职,又让血脉相连的线程默契配合。