再谈ThreadLocal

作者: jerrik | 来源:发表于2017-10-26 23:43 被阅读25次
一、引言

众所周知,ThreadLocal是java开发中不可或缺的重要类,而且它在多线程环境下能发挥举足轻重的作用。它的存储结构类似于Map<Thread,Object>,就是以当前线程作为Key的一个Map.所以不同线程之间能做到资源独立,不存在并发访问的问题。由于市面上很多讲ThreadLocal源码的文章,我这里就不再赘述了,我相信绝大部分的童鞋随便花点时间就可以理解。

二、ThreadLocal的使用场景
  1. spring的事务传播特性
    将事务沿着当前线程进行传递.
  2. 稀有资源控制访问
    比如数据库连接、分页等等都可以使用ThreadLocal来做。
  3. 分布式跟踪系统
    需要检测线程经过的所有链路的耗时情况。
三、ThreadLocal的弊端
  1. 跨线程的问题。
    由于ThreadLocal具有线程独立副本,线程之间互不干扰。所以它必然就处理不了跨线程的问题。现在考虑这么一个需求,我们先看一个spring 服务代码示例:
    public Integer doHandle(){
          Integer resultA = methodA();
          Integer resultB = longTimeMethod();//耗时服务
          Integer resultC = methodC();

          Integer resultAll = mergeResult(resultA,resultB,resultC);//合并结果集
          return resultAll;
    }
已知:

doHandle()是一个事务方法。

需求:

longTimeMethod()方法是一个耗时方法,所以要考虑把它作成异步服务.例如:

    new Thread(new Callable<Integer>(){
          public Integer call(){
                return  longTimeMethod();
          }  
    }).start();

但是这样做就会出现一个问题:
因为spring的事务是利用ThreadLocal来做的,如果在事务方法中单独再起一个线程去运行另外一个服务,则另外一个服务就获取不到原来的事务,所以只能把longTimeMethod()单独作成一个事务服务来运行.

四、谈谈InheritableThreadLocal

我们前面已经讲到ThreadLocal有跨进程的问题,也就是线程A的线程独立副本对于新创建的线程B不可见。但是为了把线程A的独立副本传递给子线程来实现更复杂的需求,JDK就实现了InheritableThreadLocal。InheritableThreadLocal继承自ThreadLocal,覆盖了ThreadLocal的getMap和createMap方法。它又是怎么实现线程之间数据的传递的呢?下面我们来说说。

  1. 先来看下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;

竟然Thread还有另外一个和threadLocals类型一样的变量,是不是很惊讶?竟然都没有关注过。那inheritableThreadLocals是在哪里被赋值的呢?我们来看看

Paste_Image.png

原来在new Thread()的时候,会将当前线程的inheritableThreadLocals传递给新创建线程的inheritableThreadLocals变量,即完成了一次"华丽的拷贝"。

  1. 再来看看InheritableThreadLocal中的几个方法
    protected T childValue(T parentValue) {
        return parentValue;
    }

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

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

所以我们在子线程中调用get()方法的时候,会从子线程中的inheritableThreadLocals取值。而inheritableThreadLocals就是父线程在new Thread()的时候传递给子线程,一切都恍然大悟!!!难怪子线程可以获取到父线程设置的属性值。下面看一个简单实例来证实一下:

final InheritableThreadLocal<SpanId> inheritableThreadLocal = new InheritableThreadLocal<SpanId>();
        inheritableThreadLocal.set(new SpanId("main thread"));
        System.out.println(inheritableThreadLocal.get().name);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("-----------");
                
                System.out.println("--->: " + inheritableThreadLocal.get().name);//父线程将属性值传递到子线程了
                inheritableThreadLocal.set(new SpanId("inner thread"));
                System.out.println(inheritableThreadLocal.get().name);
                
                System.out.println();
                
            }
        };

        ExecutorService executorService = Executors.newFixedThreadPool(10);//手动修改线程池大小,这里设置为10,保证是新创建的线程,而不是直接取池中的线程。
        executorService.submit(runnable);
        TimeUnit.SECONDS.sleep(1);
        
        System.out.println("after sleep-> " + inheritableThreadLocal.get().name);
        
        executorService.submit(runnable); //重新执行任务,又将main线程的inheritableThreadLocals属性值复制到子线程中。
        TimeUnit.SECONDS.sleep(1);
        
        System.out.println("########");
        SpanId span = inheritableThreadLocal.get();
        System.out.println("main-> " + span.name);

输出结果:
main thread
-----------
--->: main thread
inner thread

after sleep-> main thread
-----------
--->: main thread //这里依旧是main thread,因为是 new Thread(),所以将值传递进来了
inner thread

########
main-> main thread

将newFixedThreadPool中的线程数改成1再试一次。执行结果如下:

main thread
-----------
--->: main thread
inner thread

after sleep-> main thread
-----------
--->: inner thread//由于线程数固定为1,所以第二次执行任务时直接取池中的线程,不会实现独立数据副本的拷贝,故不能传递父线程的数据。只能打印原来线程的数据。
inner thread

########
main-> main thread

五、InheritableThreadLocal的弊端

InheritableThreadLocal的弊端也很明显,就是不能实现池化父子线程之间数据的传递。如果我们现在要做一个分布式链路监控系统,想依赖线程独立副本变量来做,就必须要攻克这个难关,因为在线程池大行其道的时代,随便一个框架都用了各种线程池。幸运的是,今天偶然发现阿里提供了这个问题的解决方案,那就是transmittable-thread-local.想深入研究的朋友,可以去github看它的源码。比较晚了,这里就不展开了,下次在细讲transmittable-thread-local。

参考文档:
https://github.com/alibaba/transmittable-thread-local
https://t.hao0.me/devops/2016/10/15/distributed-invoke-trace.html
https://www.tuicool.com/articles/f2qAZnZ
http://blog.csdn.net/a837199685/article/details/52712547

相关文章

网友评论

    本文标题:再谈ThreadLocal

    本文链接:https://www.haomeiwen.com/subject/fnpkpxtx.html