ThreadLocal使用与原理剖析

0. bg

最近在项目中用到threadlocal,threadLocal理解起来很简单,就是和当前线程绑定的一个map,使用get/set去拿到与线程名的key-value。那么问题来了,1.当一个线程中存在多个ThreadLocal变量,线程是如何通过get去得到value的呢?如下:

public class Test {

    ThreadLocal<String> arg1=new ThreadLocal<>();
    ThreadLocal<String> arg2=new ThreadLocal<>();
    ThreadLocal<String> arg3=new ThreadLocal<>();

    public static void main(String[] args) {
        Test test=new Test();
        test.arg1.get(); //如何去映射得到value值
        test.arg2.get();
    }
}
  1. 两个不同的类中,Test2去获取到同一个线程中的Test存储在ThreadLocal中的值的过程是什么样的?如:
public class Test2{
  //同一个线程中,去获得test中的threadLocal的值
    public static void main(String[] args) {
        Test test1=new Test();
        test1.arg1.get();
        test1.arg2.get();
    }
}

3. 如何使用ThreadLocal在同一个线程中优雅的传值呢?如问题2中,当在test1中设置了threadLocal对象的值,在test2中我们无法得到实例test1,那么我们如何去拿到threadLocal变量?

so带着上面的三个问题探索ThreadLocal.

1. ThreadLocal源码层面

从注释出发:

ThreadLocal源码注释

ThreadLocal变量通常是类中与线程关联的私有静态域:一个线程私有ThreadLocal。例如:类可以通过ThreadLocal给每个线程生成一个独一无二的标识符。
ThreadLocal源码注释

每个线程都有一个本地threadlocal副本变量的隐式的引用,当线程销毁或没有其他引用指向该线程的threaLocal的副本,则这些副本将被回收
继续往下:ThreadLocals依赖一个依附于每个线程的线性hash表,hash表中threadLocal对象(计算hash值,这个hash值是threadLocal自定义的算法)作为key。
继续往下我们发现ThreadLocal规定了未经set()而去使用get()方法获得的值为null,如果我们希望改变这get()得到的null值,可以继承ThreadLocal并重写initialValue(),当然还有内部类SuppliedThreadLocal,它重写了initialValue()方法,允许我们提供一个supplier去提供初始值。如下:

       ThreadLocal t= ThreadLocal.withInitial(()->"1");
       System.out.println(t.get()); //打印 1

现在到了最重要的方法之一:get(): 它会得到当前线程的在局部变量在threadLocal中的值,如果没有值,则调用initialValue()获得返回值。get()方法如下:

    public T get() {
        Thread t = Thread.currentThread(); //当前线程
        ThreadLocalMap map = getMap(t);//核心 threadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

对ThreadLocalMap进行研究:显然,它是一个map,每个线程都有一个threadLocalMap,那么对应上面两个问题,很难知道key是什么东西。出现问题:什么是弱引用? --弱引用对象每次GC都会被回收。 继续往下走发现,ThreadLocalMap的key就是当前的ThreadLocal对象。那么问题一二解决了,再看ThreadLocal的get源码ThreadLocalMap.Entry e = map.getEntry(this);这里的this即是当前threadLocal,故得到vaue。同时:问题:既然threadLocal只能管理一个变量,那么threadLocalMap中为什么要定义一个Entry的数组,为什么不定义一个Entry? ---> 参考上面的代码,当我们在一个类中,用到了多个ThreadLocal变量时,这个时候,每个threadLocal就在后面将key-value放到了Entry数组里面了。还应该注意到,ThreadLocal是static修饰的,那么是所有的线程公用同一个ThreadLocal对象。
set()方法同理。至于ThreadLocalMap里面其他的方法,就不累赘分析了,类似集合Map。

2. threadLocal副本?为什么threadLocal要使用变量副本?

threadLocal从字面理解,这个类为每个线程都创建了一个本地变量,实际上是ThreadLocal为变量在每个线程中都创建了一个副本,使得每个线程都可以访问自己内部的副本变量
通常提到多线程,都会考虑变量同步的问题,但是ThreadLocal并不是为了解决多线程共享变量同步的问题,而是为了让每个线程的变量不互相影响,相当于线程之间操纵的都是变量的副本,自然就不用考虑多线程竞争的问题,也自然没有性能损耗。

3. 问题三解决

通过上述分析,我们知道ThreadLocal是与线程绑定的,而线程中又共享同一份ThreadLocalMap,key-value都存储在threadLocalMap中,那么我们只要在同一份ThreadLocal操作就可以了。即把ThreadLocal做一个封装,当成类的static对象即可。如下:

public class ThreadLocalArgs {
    private static final ThreadLocal<String> argThreadLocal= new ThreadLocal<>();

    public static void set(String argument){
        argThreadLocal.set(argument);
    }


    public static String get(){
        return argThreadLocal.get();
    }

    public static void unset(){
        argThreadLocal.remove();//注意当前线程用完变量后要remove
    }
}

那么在该线程的所有实例中,通过调用ThreadLocalArgs .argThreadLocal即可拿到变量的值。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。