ThreadLocal源码解析

924 阅读5分钟

最近在研究HandlerThread源码的时候发现用到了ThreadLocal,就稍微研究了一下ThreadLocal的源码。

ThreadLocal简介

先看一下官方文档是怎么介绍ThreadLocal的:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

我简单翻译一下就是:

这个类提供了一些线程局部变量。这些变量和一般的变量不同之处就在于每一个使用到该线程局部变量的线程都有属于它自己的,经过独立初始化的,这个变量的副本。(这里的副本是什么意思,一开始我没看明白,但是后面看源码知道了,下面讲。) 线程局部变量的实例通常是私有的、静态的,在希望将状态和一个线程绑定的类里面(比如,一个用户ID或者一个交易ID)。

下面就通过源码来分析一下ThreadLocal究竟是如何制造对象副本的。

先来一个Demo

Account类中定义了一个ThreadLocal类型的变量,每个使用到该变量的线程都会保留一个该变量的副本。

class Account {
    private ThreadLocal<String> name = new ThreadLocal<>();

    public Account(String str) {
        this.name.set(str);
        System.out.println("---" + this.name.get());
    }

    public String getName() {
        return name.get();
    }

    public void setName(String str) {
        this.name.set(str);
    }
}

线程类就是我们的Demo里要创建的线程,我们会在它的run方法里试一下用一个已经被初始化的Account对象来构造MyTest线程类时会打印出什么。

class MyTest extends Thread{
    private Account account;

    public MyTest(String name, Account account) {
        super(name);
        this.account = account;
    }

    @Override
    public void run() {
        System.out.println(account.getName());
    }
}

最后是我们的主类

public class ThreadLocalTest {
    public static void main(String[] args) {
        Account at = new Account("初始名");
        new MyTest("测试线程", at).start();
    }
}

跑一下,结果如下:

---初始名
null

这说明,我们在主线程中对Account对象进行初始化以后的结果并没有影响到子线程,既然有一个创建副本的过程,创建的过程中显然创建的是一个ThreadLocal对象通过get()方法返回的那个对象的副本,这个副本显然是一个空值。

从结果上分析,过程就这么简单,那具体是怎么做的,继续看源码。

通过debug继续分析

源码中涉及到三个类:ThreadLocal, Thread, ThreadLocalMap.

我们看一下我们刚刚的demo在运行的时候程序的流程(读者可以自行debug,第一个断点加在System.out.println(account.getName()); 前面,时间有限,我就不画流程图了):

先看一下ThreadLocal的get()方法:

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

注意到ThreadLocalMap map = getMap(t); 这句话,那再看一下getMap()方法:

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

这里的threadLocals的类型就是我们刚刚提到的ThreadLocalMap,它实际上是ThreadLocal的静态内部类,他实际上是不是Map集合类,虽然取了这个名字,他是模仿Map集合类自定义的一个可以容纳键值对的类,具体看源码,这里不展开。 这个threadLocals是Thread类的field,这里可能有点乱,图我就不画了,列几个要点:

  1. threadLocals是Thread类的field

  2. ThreadLocalMap的每个Entry中,键的类型是ThreadLocal,值的类型是Object。

  3. 通过ThreadLocal的set(T value)方法给ThreadLocal对象对应的value确定值。

  4. 每个ThreadLocalMap可以维护多个键值对,即每个线程可以有多个ThreadLocal类型的变量。

OK,我们继续看刚刚的get()方法,注意到map的判定为空,为什么会为空呢(这个问题看似有点蠢,其实一般的做法是会提前先把Map集合初始化了,键值对为空没关系,但是在这里做法不同,所以强调一下),它又是在什么时候初始化呢,回头看一下ThreadLocalMap的一个构造方法:

/**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

注释上说,ThreadLocalMap的初始化是“延迟”的,一直到至少有一个entry的时候才会被create(调用ThreadLocal的void createMap(Thread t, T firstValue) 方法),其实这就是说ThreadLocalMap的初始化和第一个键值对的放入是同步的,即所谓的“懒构造”(constructed lazily)。

再次回到get()方法,注意到setInitialValue() 这个方法。

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

看到这里差不多可以明白了,initialValue() 返回的值就是null,就是说第一个键值对的value值为null,这就是为什么刚刚的结果中会打印出null的原因。

而调用createMap(t, value) 方法之后,ThreadLocalMap的初始化和第一个entry的插入同步完成了(详细可以看ThreadLocalMap对应的构造方法ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue))。

小结一下

以上便是多线程如何利用ThreadLocal创建需要访问的对象副本的关键过程,其实很简单,主要注意的是这个副本的初始值为null。

多说一句,Thread从另一个角度解决了多线程并发访问的问题,就是创建多个副本,但是它不能取代同步机制,它仅仅是隔离了多个线程之间的共享冲突,当为了实现多个线程之间的通信时,仍然需要使用同步。