Java父子线程:多线程中的血脉传承与避坑指南

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.InheritableThreadLocalInheritableThreadLocalThreadLocal的子类,专门处理父子继承关系的线程变量。

使用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 小结

父子线程如同程序世界的家族树,合理的继承机制能提升效率,而不当使用则会导致资源混乱。掌握线程间的"血缘关系",方能构建出高效稳健的并发系统。

多线程编程的艺术在于:让每个线程各司其职,又让血脉相连的线程默契配合。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智_永无止境

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值