浅谈ThreadLocal(二)

456 阅读3分钟

`紧接上篇:浅谈ThreadLocal(一)

本篇我们结合源码的方式去回答上篇文末的两个问题

  • Q1:ThreadLocal和Thread之间的关系到底如何?
  • Q2: 为什么每启动一个线程就会调用一次initialValue()呢?

一、案例回顾

1.1 自定义MyThreadLocal

在回答Q1前,我们先根据上篇中我们了解的ThreadLocal来模仿写一个MyThreadLocal。

public class MyThreadLocal<T>{
    private T t;
    protected T initialValue(){return null;}

    public T get() {
    //如果没值,则调用initialValue初始化一个值
        if (t == null){
            return initialValue();
        }
        return t;
    }
}

然后修改一下ThreadId类,改用我们刚写的MyThreadLocal去管理线程Id。

public class ThreadId {
    private static final AtomicInteger nextId = new AtomicInteger(0);
    private static final MyThreadLocal<Integer> threadId = new MyThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return nextId.getAndIncrement();
        }
    };

    public static Integer get() {
        return threadId.get();
    }

    public static void main(String[] args) {
        new Thread(() -> System.out.println(ThreadId.get())).start();
        new Thread(() -> System.out.println(ThreadId.get())).start();
        new Thread(() -> System.out.println(ThreadId.get())).start();
        new Thread(() -> System.out.println(ThreadId.get())).start();
    }
}

看运行结果,发现跟用ThreadLocal一毛一样。

image.png

什么情况,我用短短几行代码就实现了ThreadLocal的功能了?当然不是,我们如下修改一下测试代码,MyThreadLocal马上现原形。

1.2 原因分析

测试代码就不贴了,直接对比看结果。

image.png

image.png

1.3 分析源码

这时不禁更加好奇了,ThreadLocal怎么做到管理不同线程id的呢?我们结合get()源码来探寻一下

图片.png

这里我们看到冒出来两个新东西 ThreadLocalMap 和 ThreadLocalMap.Entry。这两具体内容是什么先不管,但是类比Map和Map.Entry的关系。这两个应该也大差不差。继续看get()

给大家分享个小技巧:先看代码注释,再去追源码会事半功倍。上图中作者告诉我们:这个方法返回当前线程的thread-local变量的值。如果这个变量没有值,那么就会调用initialValue()返回一个初始值。也就是说每个线程都有一个叫thread-local的变量咯?我们再证实一下Thread中是否有这个变量。发现它确实存在,但它不是ThreadLocal,而是ThreadLocalMap。

图片.png

那赶紧看看ThreadLocalMap和ThreadLocal又是怎么个事吧。先看关系图,ThreadLocal中有个内部类ThreadLocalMap,ThreadLocalMap中又套了个Entry类。

图片.png

再看源码:

图片.png

好家伙,Entry继承了WeakReference接口,泛型又是ThreadLocal。自此我们梳理一下Thread和ThreadLocal的关系。

1.4 总结

图片.png

1.4.1 ThreadLocal和Thread之间的关系

从我们分析的结果来看,ThreadLocal和Thread之间并没有直接关系。非要说,有的话也可以。因为ThreadLocal是ThreadLocalMap的key。

1.4.2 为什么每启动一个线程就会调用一次initialValue()呢?

其实我们总结出上图的关系后,在看这个问题时会恍然大悟。因为每个线程都有自己的ThreadLocalMap去存储value。每个线程在调用ThreadId.get()的时候,都会因为自己的ThreadLocalMap中没有value,而调用initialValue().因此尽管不同的线程共用一个ThreadLocal作为key,但是由于ThreadLocalMap不同,它们之间互不干扰。

最后我们知道了原来ThreadLocal不存值,我们通过它提供的get()实际上是取的Entry的value,它自己也是作为key存在Map中的。当时在使用上我们不用关心ThreadLocal的内部实现细节,只要通过它提供的set()/get()就能操作线程本地变量。是不是很像工具类的感觉。封装内部的具体实现,提供接口调用。

下篇我们接着探讨:为什么说ThreadLocal可以解决线程安全问题?Threadlocal为什么要用弱引用?ThreadLocal有哪些使用场景,要注意哪些问题? 传送门:ThreadLocal终章