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 为每个线程分别保存值的引用。它隔离的是每个线程保存的引用,不会复制引用指向的对象;不同线程若保存了同一个对象引用,该对象仍是共享的。