一步一步带你阅读ThreadLocal源码(一)—— set方法

前言

之前把《现代操作系统》的前四章看完了,收获还是很大的,尤其在进程管理那一章让我明白了并发在操作系统上是如何控制的。所以,由此也引发了这一篇源码解析文章的创作。原本是打算写AQS的源码解析的,但是最近项目比较忙,没有时间,只能先写比较简单的ThreadLocal。

PS:最近都在看《计算机网络 谢希仁编著》 ,感觉跟之前看的完全不是一本书,就是怎么变得那么简单了,可能这就是境界上的提升吧,如果LOL的段位也有那么简单提升就好了,哈哈哈哈(可能是我依然没有领悟它的精髓)。

简单使用

ThreadLocal的使用是非常简单的

public class TestThreadLocal {
    private static ThreadLocal<String> local = new ThreadLocal<>();
    private static void print(String t){
        System.out.println(t + ":" + local.get());
    }

    public static void main(String[] args) {
        Thread threadOne = new Thread(() -> {
            local.set("threadOne local variable");
            print("threadOne");
        });
        Thread threadTwo = new Thread(() -> {
            local.set("threadTwo local variable");
            print("threadTwo");
        });
        threadOne.start();
        threadTwo.start();
    }
}
复制代码

这段代码的输出结果就是

threadOne: threadOne local variable
threadTwo: threadTwo local variable

代码执行流程:

线程threadOne通过local.set("threadOne local variable")在线程内部设置了一个值,然后通过local.get()可以获取到这个在线程内部的值。

线程threadTwo同理

为什么在两个线程内部调用同一个local.get()方法却可以获取到不同的值?

在解释这个问题之前,我们先来讨论另一个问题,通过local.set()方法设置的值到底存放在哪里?

set方法

我们先来看一下java的源码,都是大佬写的,要细细的品,给大佬应有的尊重!

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}
复制代码

虽说是大佬写的代码,但是从表面上来看,还是很简单的,哈哈哈哈!

我梳理一下这段代码都做了什么:

  • 首先获取到执行该方法的线程,从上面使用的代码来看就是ThreadOne(我们只看一个线程,因为另一个线程也是一样的)。
  • 然后将该线程传递给了getMap(t)方法,那我们就来看一下这段代码做了什么
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
复制代码

这段代码返回了线程对象中的threadLocals变量。

纳尼???线程对象里面竟然还有这个变量?

没错,它确实存在着,如果你只阅读过Thread类的源码,确实可能会把这个变量忽略,因为Thread类中的绝大部分方法都是跟该变量无关的。

我使用的是jdk8,所以在Thread类中的181行就是该变量的声明。

大佬也对这个变量做了注释,大意是:该变量是与此线程有关的ThreadLocal值。而这个map是由 ThreaLocal类维护的。这里的map就是指 ThreadLocalMap

ThreadLocalMap是一个定制的Map,实际的使用上跟我们的HashMap是类似的,只是多了一个内部类Entry,该类是一个弱引用类,这个会在下一篇的get方法讲,这里就把map当作是一个我们平时使用的Map就可以了。

所以我们大概可以回答上面提出的那个问题,值到底存放在哪里?从这里,我们知道值是存放在Thread线程对象里面的。

实际上值是存在ThreadLocal的ThreadLocalMap里面,而map又存放在线程对象里面,所以间接的存放在线程对象里面

为什么要用Map来存呢?我觉得应该是因为一个线程对象可以有很多个ThreaLocal对象。List也可以存多个值,但是使用List来存的话,我们取值时就不知道应该取哪一个值了。

让我们言归正传,继续看set方法

  • 获取到map之后会进行判断:

如果map不为空,那么就将ThreadLocal对象作为键,参数value作为值,通过map的set()方法进行设置。

if (map != null) {
    map.set(this, value);
} else {
    createMap(t, value);
}
复制代码

如果map为空,那么就对map进行初始化。

void createMap(Thread t, T firstValue) {
    // 创建一个ThreadLocalMap,并且设置第一个键值对,等同于map.set(this, value)
    // 然后赋值给当前线程对象的threadLocals变量
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码

map将ThreadLocal对象作为键,那我们就可以大胆猜测测试代码中local.get()方法是怎么获取值的。

  • 首先获取当前线程对象
  • 然后获取当前线程中的threadLocals变量
  • 因为threadLocals是一个以ThreadLocal对象作为键的map结构,所以我们只要将this(调用get()的ThreadLocal对象)作为参数传递到map的get方法中就可以获取到对应的值。

ThreadLocal的get()基本步骤是这样的,但是因为传递进来的value实际上是要被弱引用的,所以具体实现会有一些不同。至于弱引用我会在后面单独作为一个专栏来讲。

结论

我们通过set方法可以总结几点:

  • ThreadLocal对象内部是通过Map结构存储传递进来的值的,以ThreadLocal对象作为键,vlaue作为值进行存储。
  • Map结构是存储在线程对象内部的,所以我们获取值的时候先通过线程对象获取到Map,然后通过ThreadLocal作为键在Map中获取到值。
  • 一个线程对象是可以有多个ThreadLocal对象的,但是一个ThreadLocal对象只能对应一个值。所以如果要在一个线程中复制多个全局变量的副本,就要创建多个ThreadLocal对象。

尾声

终于写完了,明天我会把get方法也一起写出来!如果有疑问的话,可以直接评论区一起讨论哦,知无不言。后续还会写出几篇与ThreadLocal有关的专栏,包括剩下的一些比较重要的方法,弱引用,应用场景,ThreadLocal如何造成内存泄漏。

猜你喜欢

转载自juejin.im/post/5df89774e51d4557ea02b256