ThreadLocal分析五:InheritableThreadLocal

通过上面几篇文章的分析,大家已经了解了threadlocal的数据结构,以及源码分析。但是他有什么缺点呢?

答案是threadlocal不能在父子线程中线程之间传递

public class ThreadLocalTest {
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Integer reqId = new Integer(5);
        ThreadLocalTest tt = new ThreadLocalTest();
        tt.setReqId(reqId);
    }
    public static void  setReqId(Integer integer){
        threadLocal.set(integer);
        doBussiness();
    }

    private static void doBussiness() {
        System.out.println("首先打印reqId"+threadLocal.get());
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程启动");
                System.out.println("在子线程获取reqId"+threadLocal.get());
            }
        }).start();
    }
}

输出:

 可以看到子线程无法访问父线程中设置的本地变量。那我们该如何解决这么问题?

为了解决这个问题,jdk引入了另外一个线程本地变量实现类InheritableThreadLocal,接下来介绍InheritableThreadLocal实现的原理。

为了解决上述问题,JDK引入了InheritableThreadLocal,即子线程可以访问父线程中的线程本地变量,更严谨的说法是子线程可以访问在创建子线程时父线程当时的本地线程变量,因为其实现原理就是在创建子线程的时候将父线程当前存在的本地线程变量拷贝到子线程的本地线程变量中。

从类的继承层次来看,InheritableThreadLocal只是在ThreadLocal的get、set、remove流程中,重写了getMap、createMap方法,整体流程与ThreadLocal保持一致,故我们初步来看一下InheritableThreadLocal是如何重写上述这两个方法的。

我们看一下InheritableThreadLocal如何重写getMap 和createMap的方法

ThreadLocal getMap方法和createMap对应的是自己的

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }


    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

InheritableThreadLocal的getMap方法和createMap方法

    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);
    }

可以看到 getMap返回的是当前线程的inheritableThreadLocals ,createMap是新创建一个ThreadLocalMap.

那问题来了,InheritableThreadLocal是如何继承自父对象的线程本地变量的呢?

子线程是通过在父线程中通过调用new Thread()方法来创建子线程,Thread#init方法在Thread的构造方法中被调用。

        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

如果父线程的inheritableThreadLocals不为空并且inheritThreadLocals为true(该值默认为true),则使用父线程的inherit本地变量的值来创建子线程的inheritableThreadLocals结构,即将父线程中的本地变量复制到子线程中。

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { 
 return new ThreadLocalMap(parentMap);
}
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) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) 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++;
            }
        }
    }
}

上述代码就不一一分析,类似于Map的复制,只不过其在Hash冲突时,不是使用链表结构,而是直接在数组中找下一个为null的槽位。

子线程默认拷贝父线程的方式是浅拷贝,如果需要使用深拷贝,需要使用自定义ThreadLocal,继承InheritableThreadLocal并重写childValue方法。

验证InheritableThreadLocal的特性

public class ThreadLocalTest {
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    private static InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
    public static void main(String[] args) {
        Integer reqId = new Integer(5);
        ThreadLocalTest tt = new ThreadLocalTest();
        tt.setReqId(reqId);
    }
    public static void  setReqId(Integer integer){
        inheritableThreadLocal.set(integer);
        doBussiness();
    }

    private static void doBussiness() {
        System.out.println("首先打印reqId"+inheritableThreadLocal.get());
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程启动");
                System.out.println("在子线程获取reqId"+inheritableThreadLocal.get());
            }
        }).start();
    }
}

结果:

 那InheritableThreadLocal有没有局限性呢?

InheritableThreadLocal支持子线程访问在父线程中设置的线程上下文环境的实现原理是在创建子线程时将父线程中的本地变量值复制到子线程,即复制的时机为创建子线程时。

但我们提到并发、多线程就离不开线程池的使用,因为线程池能够复用线程,减少线程的频繁创建与销毁,如果使用InheritableThreadLocal,那么线程池中的线程拷贝的数据来自于第一个提交任务的外部线程,即后面的外部线程向线程池中提交任务时,子线程访问的本地变量都来源于第一个外部线程,造成线程本地变量混乱。也就是说,inheritablethreadlocal只赋值了第一个外部线程,而其他的外部线程,他就获取不到了。

public class ThreadLocalTest {
    //模拟tomcat线程池
    private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);
    //业务线程池 默认Control中异步任务执行线程池
    private static ExecutorService businessExecutors =Executors.newFixedThreadPool(5);
    //线程上下文环境,模拟在Control这一层,设置环境变量,然后在这里提交一个异步任务,模拟在子线程中,是否可以访问到刚设置的环境变量值。
    private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        for (int i = 0; i <10 ; i++) {
            // 模式10个请求,每个请求执行ControlThread的逻辑,其具体实现就是,先输出父线程的名称,
            // 然后设置本地环境变量,并将父线程名称传入到子线程中,在子线程中尝试获取在父线程中的设置的环境变量
            tomcatExecutors.submit(new ControlThread(i));
        }
    }

    /**
     * 模拟Control任务
     */
    static class ControlThread implements Runnable {
        private int i;

        public ControlThread(int i) {
            this.i = i;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            requestIdThreadLocal.set(i);
            businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));
        }
    }

    /**
     * 业务任务,主要模拟Control控制层,提交任务到线程池执行
     */
    static class BusinessTask implements Runnable {
        private String parentThreadName;

        public BusinessTask(String parentThreadName) {
            this.parentThreadName = parentThreadName;
        }

        @Override
        public void run() {
            System.out.println("parentThreadName:"+parentThreadName+":"+requestIdThreadLocal.get());
        }
    }
}

结果:

 可以看到 下面的值是错乱的,pool-1-thread-3 应该是2 但是在子线程里面却是9

在子线程中出现出现了线程本地变量混乱的现象,在全链路跟踪与压测出现这种情况是致命的。

TransmittableThreadLocal

TransmittableThreadLocal何许人也,它可是阿里巴巴开源的专门解决InheritableThreadLocal的局限性,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程变量。实践是检验整理的唯一标准,我们还是以上面的示例来进行验证,看看TransmittableThreadLocal是否支持上述场景:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.10.2</version>
</dependency>
package com.example.demo.empty;

import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.threadpool.TtlExecutors;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalTest {
    /**
     * 模拟tomcat线程池
     */
    private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);

    /**
     * 业务线程池,默认Control中异步任务执行线程池
     */
    private static ExecutorService businessExecutors = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(4)); // 使用ttl线程池,该框架的使用,请查阅官方文档。

    /**
     * 线程上下文环境,模拟在Control这一层,设置环境变量,然后在这里提交一个异步任务,模拟在子线程中,是否可以访问到刚设置的环境变量值。
     */
    private static TransmittableThreadLocal<Integer> requestIdThreadLocal = new TransmittableThreadLocal<>();

//    private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            tomcatExecutors.submit(new ControlThread(i));
        }

        //简单粗暴的关闭线程池
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        businessExecutors.shutdown();
        tomcatExecutors.shutdown();

    }


    /**
     * 模拟Control任务
     */
    static class ControlThread implements Runnable {
        private int i;

        public ControlThread(int i) {
            this.i = i;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            requestIdThreadLocal.set(i);

            //使用线程池异步处理任务

            businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));


        }
    }

    /**
     * 业务任务,主要是模拟在Control控制层,提交任务到线程池执行
     */
    static class BusinessTask implements Runnable {
        private String parentThreadName;

        public BusinessTask(String parentThreadName) {
            this.parentThreadName = parentThreadName;
        }

        @Override
        public void run() {
            //如果与上面的能对应上来,则说明正确,否则失败
            System.out.println("parentThreadName:" + parentThreadName + ":" + requestIdThreadLocal.get());
        }
    }
}

结果是:

pool-1-thread-1:0
pool-1-thread-6:5
pool-1-thread-4:3
pool-1-thread-5:4
pool-1-thread-3:2
pool-1-thread-2:1
pool-1-thread-7:6
pool-1-thread-8:7
pool-1-thread-9:8
pool-1-thread-10:9
parentThreadName:pool-1-thread-8:7
parentThreadName:pool-1-thread-7:6
parentThreadName:pool-1-thread-4:3
parentThreadName:pool-1-thread-2:1
parentThreadName:pool-1-thread-6:5
parentThreadName:pool-1-thread-9:8
parentThreadName:pool-1-thread-5:4
parentThreadName:pool-1-thread-10:9
parentThreadName:pool-1-thread-3:2
parentThreadName:pool-1-thread-1:0

Process finished with exit code 0

TransmittableThreadLocal实现原理

从InheritableThreadLocal不支持线程池的根本原因是InheritableThreadLocal是在父线程创建子线程时复制的,由于线程池的复用机制,“子线程”只会复制一次。要支持线程池中能访问提交任务线程的本地变量,其实只需要在父线程向线程池提交任务时复制父线程的上下环境,那在子线程中就能够如愿访问到父线程中的本地变量,实现本地环境变量在线程池调用中的透传,从而为实现链路跟踪打下坚实的基础,这也就是TransmittableThreadLocal最本质的实现原理。

具体的使用方法大家可以去官网查看一下https://github.com/alibaba/transmittable-thread-local

  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值