ThreadLocal 源码分析

在并发编程的时候,常常会遇到共享变量,通常为了保证数据的 一致性 ,需要对变量加锁,这就导致了执行效率的降低。如果数据确实只由单个线程访问,就不需要为它与其他线程同步;如果多个线程会访问同一可变对象,仍需考虑同步。

ThreadLocal 为每个线程分别保存一个值的引用。线程之间通过同一个 ThreadLocal 读写各自的值时互不影响,但 ThreadLocal 不会复制对象;如果多个线程持有同一个可变对象的引用,对它的修改仍然会互相影响。下面通过示例和源码来说明。

提供的API

正式开始之前,先看看 ThreadLocal 这个类给我提供了哪些API

  • set(T value)
    为当前线程设置 ThreadLocal 的值引用
  • T get()
    获取当前线程保存的 ThreadLocal 值;若该线程尚未设置值,则调用 initialValue()
  • remove()
    移除当前线程保存的值
  • T initialValue()
    当前线程首次调用 get() 且尚无值时调用,默认返回 null。调用 remove() 后,该线程再次调用 get() 时也会重新调用它

下面简单使用一下:

fun main(args: Array<String>) {
    val str = "init value"
    val tl = ThreadLocal<String>()
    tl.set(str)
    val thread = Thread{
        tl.set("child's value")
        println(tl.get())
    }
    thread.start()
    thread.join()//让子线程执行完
    println(tl.get())
}

然后我们的预期结果是:

 
child's value
init value
 

运行之后的结果和上面一样。

注意点

如果上面的代码没什么问题,我们接着写另一个例子。 为了演示方便,我们先定义一个 Foo 类:

data class Foo(var name:String)

然后再看我们的 main 函数:

fun main(args: Array<String>) {
    val foo = Foo("init value")
    val tl = ThreadLocal<Foo>()
    tl.set(foo)
    val thread = Thread {
        tl.get().name = "changed in child"
        println(tl.get())
    }
    thread.start()
    thread.join()//保证线程执行完毕
 
    println(tl.get())
    //预期结果:
    //changed in child
    //init value
}

运行一下,Oops,崩了!!! 对比一下之前的例子,会发现第二个例子在子线程中首先通过 get() 获取 foo 实例。这里就存在一个问题,回过头看 initialValue() 函数的注释,发现这个线程正好符合它被调用的条件,所以会得到一个 NullPointerException。

既然 initialValue() 返回了一个 null 值,那么我们重写这个方法,返回一个合法值。改造之后的代码:

fun main(args: Array<String>) {
    val tl = object:ThreadLocal<Foo>(){
        override fun initialValue(): Foo = Foo("init value")
    }
    val thread = Thread {
        tl.get().name = "changed in child"
        println(tl.get())
    }
    thread.start()
    thread.join()//保证线程执行完毕
 
    println(tl.get())
    //预期结果:
    //changed in child
    //init value
}

运行一下,打印结果也确实和预期结果一致。不过还有个问题,如果我们执行 println(tl.get().hashCode()) ,会发现两个对象不一样,因为不同线程的初始值都是新创建的 Foo 对象。有同学可能就会想到这样改:

val tl = object:ThreadLocal<Foo>(){
val foo = Foo("init value")
override fun initialValue(): Foo = foo
}

这样不同线程的初始值就是同一个实例。虽然每个线程分别保存这个引用,但它们指向同一个可变对象,因此并发修改仍可能产生数据竞争;只有为每个线程创建独立对象,才能隔离对象状态。

源码分析

ThreadLocal 这个类还是比较简单,只有几个关键函数,我们一个个来分析:

T 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 中存在以 this 为 key 的 Entry,就返回其中的值;即使该值为 null 也会直接返回
  • 如果 map 不存在,或 map 中没有这个 key,调用 initialValue() 并将结果保存后返回

这里的 ThreadLocalMap 可以简单地看作一个散列表。Entry 是 WeakReference 的子类,key 使用弱引用;但 value 仍是强引用,因此在线程长期存活且 ThreadLocal key 被回收时,value 可能暂时滞留,不能据此认为它能保证不会发生内存泄漏。

set(T value)

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

set() 所做的工作非常简单:

  • 获取当前线程的 ThreadLocalMap 对象
  • 如果 ThreadLocalMap 已存在,就将 value 存入以 this 为 key 的 Entry;如果不存在,则为当前线程创建 map 并存入 value

remove()

public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
}

拿到当前线程的 ThreadLocalMap,若存在就移除与 this 对应的 Entry。对当前线程而言,之后再次调用 get() 会重新进入初始化流程;其他线程保存的值不受影响。

关于线程私有

对象通常存放在共享的 heap(堆内存)中,但 ThreadLocal 为每个线程分别保存值的引用。它隔离的是每个线程保存的引用,不会复制引用指向的对象;不同线程若保存了同一个对象引用,该对象仍是共享的。