文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

InheritableThreadLocal 是如何实现的父子线程局部变量的传递

2024-11-29 20:45

关注

相信对并发编程有一定了解的同学已经想到了大名鼎鼎的 ThreadLocal 了,是的,线程内部就是通过 inheritableThreadLocals 实现了父子线程间局部变量的传递。

JDK 8

一、父子线程间局部变量参数传递的方式 ThreadLocal

首先我们写看一段代码:


public class ThreadLocalTest implements Runnable{
    private static final InheritableThreadLocal MAIN_THREAD_LOCAL = new InheritableThreadLocal<>();
    @SneakyThrows
    @Override
    public void run() {
        System.out.println("threadlocal 默认值:"+ThreadLocalTest.MAIN_THREAD_LOCAL.get());
        MAIN_THREAD_LOCAL.set("child thread value :"+Thread.currentThread().getName());
        System.out.println("threadlocal 设置子线程值之后:"+ThreadLocalTest.MAIN_THREAD_LOCAL.get());
    }
    public String get(){
        return MAIN_THREAD_LOCAL.get();
    }
    public void clean(){
        MAIN_THREAD_LOCAL.remove();
    }
    public static void main(String[] args) {
        ThreadLocalTest threadLocalTest = new ThreadLocalTest();
        MAIN_THREAD_LOCAL.set("父线程的值 set 111");
        System.out.println("启动:"+threadLocalTest.get());
        for (int i = 0; i < 3; i++) {
            new Thread(threadLocalTest).start();
//            ThreadUtil.execAsync(threadLocalTest);
        }
        System.out.println("结束:"+threadLocalTest.get());
    }
}

在上面的这段代码中,我们就做了三个事情:

大家可以先猜一下这段代码的运行结果。

二、子线程可以继承父线程局部变量的值吗

首先我们先说下答案,是可以继承的。上面代码的执行结果如下:

启动:父线程的值 set 111
结束:父线程的值 set 111
threadlocal 默认值:父线程的值 set 111
threadlocal 设置子线程值之后:child thread value :Thread-1
threadlocal 默认值:父线程的值 set 111
threadlocal 默认值:父线程的值 set 111
threadlocal 设置子线程值之后:child thread value :Thread-2
threadlocal 设置子线程值之后:child thread value :Thread-0

在上面的代码中,我们的子线程优先打印了父线程中ThreadLocal的值,然后重新设置该值,再次读取。得出结论就是子线程可以通过ThreadLocal继承父线程的值,并且子线程自己内容再次重新设置不影响父线程的值。

三、父子线程局部变量传值的原理

难道一句简单的ThreadLocal就可以让我们对这个问题停止探索吗?那么线程内部是如何通过ThreadLocal进行传值的呢?

1.new thread

在上面代码中,启动子线程的方式是new Thread(threadLocalTest).start();,所以秘密一定就在这一行代码里面。源码之下无秘密,我们一起来看下。

首先进入new Thread()的内部:

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

通过上面两个方法调用,最终进入到下面这个方法中:

 private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {}

init方法有个参数inheritThreadLocals,boolean类型的,如果为true,且可继承的线程局部变量不为空就继承。

现在我们只需要顺着inheritThreadLocals这个参数去找就可以了,在Thread的418行,有这样一行代码。(代码行数可能因版本而位置不同)

可以看到是直接对当前线程的inheritableThreadLocals直接进行的赋值操作,而值是通过ThreadLocal.createInheritedMap获取的,下面我们看下这个createInheritedMap方法做了哪些操作?

createInheritedMap方法是ThredLocal内部的方法,接收传递父线程的ThreadLocalMap为参数,该方法只做了一个事情,就是new了一个新的ThreadLocalMap。

跟进到new ThreadLocalMap(parentMap)方法内部,其实是把传进的值,一个个的遍历进行赋值到当前线程中。

对于图中标记的第二个地方,childValue调用的是InheritableThreadLocal#childValue,该方法内也只做了一件事,就是返回传进来的值。

(1) 小结

父子线程之所以能传参,是因为我们使用了InheritableThreadLocal,这样在new Thread()时,就会进入到给子线程赋值父线程inheritableThreadLocals的逻辑中去。

(2) 扩展

有的同学会说了,我用 ThreadLocal.withInitial创建的,怎么走到线程的if (inheritThreadLocals && parent.inheritableThreadLocals != null)判断时,没有进去呢,上面不是说是在这判断然后对子线程进行赋值的吗?

在这简单说一下哈,大家在写代码时,或者再用第三方框架时,源码中的注释一定要看仔细,很多细节都在注释中标注清楚了。

    public static ThreadLocal MAIN_THREAD_LOCAL = ThreadLocal.withInitial(() -> "父线程的值 withInitial 111");

在上面的代码中,我们进行了ThreadLocal的初始化赋值,然后看下withInitial方法。

所以是当调用get方法时,才会触发赋值的操作,那么我们看下get方法。

如果当前线程的局部变量没有值,返回初始化方法初始的值。

所以对于我们来说就是SuppliedThreadLocal#initialValue返回的值。

2.线程池

刚才我们是通过new Thread()启动的子线程,可是工作中基本都是通过线程池的方式执行任务的啊,那还生效吗?

答案是生效。

我们使用hutool工具中的ThreadUtil.execAsync(threadLocalTest);进行测试。

直接说结论,感兴趣的同学可以自行修改一下代码中的子线程启动方式。

先画个流程图,大家可以跟着代码走一下。

当使用线程池时,底层原理还是线程池中放入任务的逻辑,当放入线程池之后,会在AbstractExecutorService#submit()方法中执行execute方法,最终执行在ThreadPoolExecutor#execute(),在这里,就是把任务丢入线程池工作的逻辑,其中有个方法addWorker,该方法中有一行new Worker(),而在该Worker方法的内部,其实就是new Thread(),到了这,就与上面所说的一样了,到了判断inheritableThreadLocals的时候了。

四、如何解决内存泄漏

使用ThreadLocal的应用场景有很多,父子线程传参数的场景也有不少,但是有一个很关键的点内存溢出是需要重视的。解决ThreadLocal内存溢出的方式也很简单,就是在使用完成之后调用一下remove。

对于上面的代码示例,就是调用我们的clean方法。

public void clean(){

 MAIN_THREAD_LOCAL.remove();

}

remove的代码如下,取值不为null时,执行删除逻辑。

五、总结

我们通过一个示例,验证了父子线程间可以通过ThreadLocal进行传递,测试了不同方式初始化ThreadLocal,并对比了new Thread()与线程池启动的区别。

其实殊途同归,线程池最后调用的还是Thread里面的方法。唯一需要注意的就是通过ThreadLocal.withInitial初始化是在get时赋值的,不过这个应该也不重要,了解一下就好,应该也没有面试官会这么抠这个问题吧。

来源:醉鱼Java内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯