ThreadLocal父子线程数据传递方案(修正篇)

前言

介绍InheritableThreadLocal之前,假设读者对 ThreadLocal 已经有了一定的理解,比如基本概念、原理等。在讲解之前我们先列举有关ThreadLocal的几个关键点。

每一个Thread线程都有属于自己的ThreadLocalMap,里面有一个弱引用的Entry(ThreadLocal,Object),如下:

Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
    }

从ThreadLocal中get值的时候,首先通过Thread.currentThread得到当前线程,然后拿到这个线程的ThreadLocalMap。再传递当前ThreadLocal对象(结合上一点),取得Entry中的value值。

set值的时候同理,更改的是当前线程的ThreadLocalMap的Entry中key为当前Threadlocal对象的value值。

Threadlocal bug?

如果子线程想要拿到父线程的中的ThreadLocal值怎么办呢?比如会有以下的这种代码的实现。由于ThreadLocal的实现机制,在子线程中调用get时,我们拿到的Thread对象是当前子线程对象,那么他的ThreadLocalMap是null的,所以我们得到的value也是null。(注:原文举的例子有错误,沉思君重写了示例代码)

public class ThreadLocalTest {

    public static void main(String[] args) throws  Exception{
        final ThreadLocal<String> threadLocal=new ThreadLocal<String>();
        threadLocal.set("Java架构沉思录");
        System.out.println("父线程的值:"+threadLocal.get());
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程的值:"+threadLocal.get());
            }
        }).start();

        Thread.sleep(2000);
    }
}

结果输出如下:

InheritableThreadLocal实现

那其实很多时候我们是有子线程获得父线程ThreadLocal的需求的,要如何解决这个问题呢?这就是InheritableThreadLocal这个类所做的事情。先来看下InheritableThreadLocal所做的事情。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * Computes the child's initial value for this inheritable thread-local
     * variable as a function of the parent's value at the time the child
     * thread is created.  This method is called from within the parent
     * thread before the child is started.
     * <p>
     * This method merely returns its input argument, and should be overridden
     * if a different behavior is desired.
     *
     * @param parentValue the parent thread's value
     * @return the child thread's initial value
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

以上代码大致的意思就是,如果你使用InheritableThreadLocal,那么保存的所有东西都已经不在原来的t.thradLocals里面,而是在一个新的t.inheritableThreadLocals变量中了。下面是Thread类中两个变量的定义。

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

Q:InheritableThreadLocal是如何实现在子线程中能拿到当前父线程中的值的呢?

A:一个常见的想法就是把父线程的所有的值都copy到子线程中。

下面来看看在线程new Thread的时候线程都做了些什么?

private void init(ThreadGroup g, Runnable target, String name,
                     long stackSize, AccessControlContext acc) {
       //省略上面部分代码
       if (parent.inheritableThreadLocals != null)
       //这句话的意思大致不就是,copy父线程parent的map,创建一个新的map赋值给当前线程的inheritableThreadLocals。
           this.inheritableThreadLocals =
               ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
      //省略下面部分代码
   }

而且,在copy过程中是浅拷贝,key和value都是原来的引用地址。

private ThreadLocalMap(ThreadLocalMap parentMap) {
           Entry[] parentTable = parentMap.table;
           int len = parentTable.length;
           setThreshold(len);
           table = new Entry[len];
           for (int j = 0; j < len; j++) {
               Entry e = parentTable[j];
               if (e != null) {
                   ThreadLocal key = e.get();
                   if (key != null) {
                       Object value = key.childValue(e.value);
                       Entry c = new Entry(key, value);
                       int h = key.threadLocalHashCode & (len - 1);
                       while (table[h] != null)
                           h = nextIndex(h, len);
                       table[h] = c;
                       size++;
                   }
               }
           }
       }

恩,到了这里,大致的解释了一下InheritableThreadLocal为什么能解决父子线程传递Threadlcoal值的问题。

  1. 在创建InheritableThreadLocal对象的时候赋值给线程的t.inheritableThreadLocals变量。
  2. 在创建新线程的时候会check父线程中t.inheritableThreadLocals变量是否为null,如果不为null则copy一份ThradLocalMap到子线程的t.inheritableThreadLocals成员变量中去。
  3. 因为覆写了getMap(Thread)和CreateMap()方法,所以get的时候,就可以在getMap(t)的时候就会从t.inheritableThreadLocals中拿到map对象,从而实现了可以拿到父线程ThreadLocal中的值。

so,在最开始的代码示例中,如果把ThreadLocal对象换成InheritableThreadLocal对象,看看结果如何。

public class ThreadLocalTest {

    public static void main(String[] args) throws  Exception{
        final ThreadLocal<String> threadLocal=new InheritableThreadLocal<>();
        threadLocal.set("Java架构沉思录");
        System.out.println("父线程的值:"+threadLocal.get());
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程的值:"+threadLocal.get());
            }
        }).start();

        Thread.sleep(2000);
    }
}

输出结果如下:

可以看到,使用了InheritableThreadLocal后,在子线程中可以拿到父线程设置的值了。

InheritableThreadLocal还有问题吗?

问题场景

我们在使用线程的时候往往不会只是简单的new Thrad对象,而是使用线程池,当然线程池的好处多多。这里不详解,既然这里提出了问题,那么线程池会给InheritableThreadLocal带来什么问题呢?我们列举一下线程池的特点:

  1. 为了减小创建线程的开销,线程池会缓存已经使用过的线程
  2. 生命周期统一管理,合理的分配系统资源

对于第一点,如果一个子线程已经使用过,并且会set新的值到ThreadLocal中,那么第二个task提交进来的时候还能获得父线程中的值吗?比如下面这种情况(虽然是线程,用sleep尽量让他们串行的执行)。(注:原文的示例代码不完成,沉思君重写了示例代码)

public class InheritableThreadLocalTest {

    public static void main(String[] args) throws Exception{
        final ThreadLocal<Person> threadLocal=new InheritableThreadLocal<>();
        threadLocal.set(new Person("Java架构沉思录"));
        System.out.println("初始值:"+threadLocal.get());
        Runnable runnable=()->{
            System.out.println("----------start------------");
            System.out.println("父线程的值:"+threadLocal.get());
            threadLocal.set(new Person("沉思君"));
            System.out.println("子线程覆盖后的值:"+threadLocal.get());
            System.out.println("------------end---------------");
        };
        ExecutorService executorService= Executors.newFixedThreadPool(1);
        executorService.submit(runnable);
        TimeUnit.SECONDS.sleep(1);
        executorService.submit(runnable);
        TimeUnit.SECONDS.sleep(1);
        executorService.submit(runnable);
    }
}

注意,这里为了看到线程复用的效果,创建了一个只有1个线程的线程池,如果创建的线程池有多个线程,可能看不到线程复用的效果。运行示例代码,输出如下:

可以看到,当第一个线程覆盖了父线程的值后,后面的子线程就拿不到父线程的值了。

造成这个问题的原因是什么呢,下图大致讲解一下整个过程的变化情况,如图所示,由于B任务提交的时候使用了A任务的缓存线程,A缓存线程的InheritableThreadLocal中的value已经被更新了。B任务在代码内获得值的时候,直接从t.InheritableThreadLocal中获得值,所以就获得了线程A中设置的值,而不是父线程中InheritableThreadLocal的值。

so,InheritableThreadLocal还是不能够解决线程池当中获得父线程中ThreadLocal中的值。

造成问题的原因

那么造成这个问题的原因是什么呢?如何让任务之间使用缓存的线程不受影响呢?实际原因是,我们的线程在执行完毕的时候并没有清除ThreadLocal中的值,导致后面的任务重用已有的threadLocalMap。

解决方案

如果我们能够,在使用完这个线程的时候清除所有的threadLocalMap,在submit新任务的时候在重新从父线程中copy所有的Entry。然后重新给当前线程的t.inhertableThreadLocal赋值。这样就能够解决在线程池中每一个新的任务都能够获得父线程中ThreadLocal中的值而不受其他任务的影响,因为在生命周期完成的时候会自动clear所有的数据。Alibaba的一个库解决了这个问题github:alibaba/transmittable-thread-local。

transmittable-thread-local实现原理

如何使用

这个库最简单的方式是这样使用的,通过简单的修饰,使得提交的runable拥有了上一节所述的功能。具体的API文档详见github,这里不再赘述。(注:原文示例代码不完整,沉思君对其进行重写)

public class TransmittableThreadLocalTest{

    public static void main(String[] args)  throws Exception{
        final ThreadLocal<Person> threadLocal=new TransmittableThreadLocal<>();
        threadLocal.set(new Person("Java架构沉思录"));
        System.out.println("初始值:"+threadLocal.get());
        Runnable task=()->{
            System.out.println("----------start------------");
            System.out.println("父线程的值:"+threadLocal.get());
            threadLocal.set(new Person("沉思君"));
            System.out.println("子线程覆盖后的值:"+threadLocal.get());
            System.out.println("------------end---------------");
        };
        ExecutorService executorService= Executors.newFixedThreadPool(1);
        Runnable runnable= TtlRunnable.get(task);
        executorService.submit(runnable);
        TimeUnit.SECONDS.sleep(1);
        executorService.submit(runnable);
        TimeUnit.SECONDS.sleep(1);
        executorService.submit(runnable);
    }
}

结果输出如下:

原理简述

这个方法TtlRunnable.get(task)最终会调用构造方法,返回的是该类本身,也是一个Runable,这样就完成了简单的装饰。最重要的是在run方法这个地方。

public final class TtlRunnable implements Runnable {
    private final AtomicReference<Map<TransmittableThreadLocal<?>, Object>> copiedRef;
    private final Runnable runnable;
    private final boolean releaseTtlValueReferenceAfterRun;

    private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        //从父类copy值到本类当中
        this.copiedRef = new AtomicReference<Map<TransmittableThreadLocal<?>, Object>>(TransmittableThreadLocal.copy());
        this.runnable = runnable;//提交的runable,被修饰对象
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }
    /**     
      * wrap method {@link Runnable#run()}.    
      */
    @Override
    public void run() {
        Map<TransmittableThreadLocal<?>, Object> copied = copiedRef.get();
        if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
        //装载到当前线程
        Map<TransmittableThreadLocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
        try {
            runnable.run();//执行提交的task
        } finally {
        //clear
        TransmittableThreadLocal.restoreBackup(backup);
        }
    }}

在上面的使用线程池的例子当中,如果换成这种修饰的方式进行操作,B任务得到的肯定是父线程中ThreadLocal的值,解决了在线程池中InheritableThreadLocal不能解决的问题。

原文:https://blog.csdn.net/a837199685/article/details/52712547
Java架构沉思录做了部分修改和删减。

原文发布于微信公众号 - Java架构沉思录(code-thinker)

原文发表时间:2018-05-31

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏ImportSource

设计ThreadLocal的那段日子

假设现在让你去实现一个连接类,要支持多个线程访问,同时每个线程独占一个Connection? 这时候你会怎么实现? 也许这会你想到了给每个线程传递一个Conne...

3516
来自专栏刘望舒

谈谈那个听起来挺高大上的ThreadLocal

1122
来自专栏技术小黑屋

理解Java中的ThreadLocal

提到ThreadLocal,有些Android或者Java程序员可能有所陌生,可能会提出种种问题,它是做什么的,是不是和线程有关,怎么使用呢?等等问题,本文将总...

1034
来自专栏Java编程技术

使用ThreadLocal不当可能会导致内存泄露

基础篇已经讲解了ThreadLocal的原理,本节着重来讲解下使用ThreadLocal会导致内存泄露的原因,并讲解使用ThreadLocal导致内存泄露的案例...

851
来自专栏风中追风

深入理解Threadlocal 关于内存泄漏的思考

什么是内存泄漏呢?对象已经没有在其它地方被使用了,但是垃圾回收器没办法移除它们,因为还在被引用着。

43213
来自专栏编程微刊

【前端统计图】hcharts实现堆叠柱形图(与后台数据交互)

3885
来自专栏Java学习123

原 Java中计算程序运行耗时的方法对比

2753
来自专栏小怪聊职场

爬虫课堂(二十七)|使用scrapy-redis框架实现分布式爬虫(2)源码分析

5906
来自专栏javathings

何时该使用 ThreadLocal,它的工作原理是什么(面试必背)?

ThreadLocal 的概念,面试的时候容易被问到。它的概念很简单,从类的名字就可以知道,线程本地变量的意思。即该变量运行在线程中时,每个线程都独立拥有它而不...

2782
来自专栏Java技术分享

保存到配置文件

/** * 保存查询京东订单的开始时间与结束时间 * * @param startDate * @param endTime * @return * @thro...

3657

扫码关注云+社区

领取腾讯云代金券