0%

令人费解的ThreadLocal

相关代码来自java 8

ThreadLocal一直给人一种奇怪的感觉:既是多线程共享的变量,每个线程有拥有独立的变量值

可以看这段测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};

public static void main(String[] args) {
Thread t1 = new Thread("thread1") {
@Override
public void run() {
threadLocal.set(1);
System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
}
};

Thread t2 = new Thread("thread2") {
@Override
public void run() {
System.out.println(threadLocal.get());
}
};

t1.start();
t2.start();
}

以上可能输出(顺序不一定):

1
2
thread1: 1
0

简单来说:ThreadLocal并不存储值,也并不是一个存储值的变量;一个ThreadLocal实例,作为共享变量关联到多个线程,每个线程中,都通过Map的数据结构,建立一个线程私有的、以这一ThreadLocal实例为key的存储,从而可以使用这一ThreadLocal实例查询、操作线程私有的Map,使用了多个ThreadLocal对象时,就相当于Map中多个节点。所以,ThreadLocal实例是作为key共享的,但是每个线程的Map映射值是线程私有的

给人奇怪的感觉只是因为,ThreadLocal了提供的方法名叫set/get,给人的感觉是ThreadLocal实例负责存储值

线程的ThreadLocalMap

每个线程维护了一个ThreadLocalMap实例threadLocals

ThreadLocalMap及内部的Entry类,与HashMap的数据结构类似,实现了一个Map:

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

Entry是一个WeakReference的子类,维护一个对ThreadLocal对象的弱引用,并使用这一ThreadLocal对象作为key

可以看出,Entry和HashMap的Node不同:Entry不包括下一个Entry的指针,所以ThreadLocalMap是使用Entry[]进行存储,而HashMap是数组+链表的形式;在发生冲突时,Entry查找数组的下一个位置直到找到一个可用的位置,即线性探查法,而HashMap是链地址法

在ThreadLocal类set()方法中,通过Thread.currentThread()取得了Thread对象,Thread对象的threadLocals field存储了这一线程关联的ThreadLocalMap,以this(调用方法的ThreadLocal对象的引用)作为key添加到Map中

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

在ThreadLocal类get()方法类似,也是使用this引用(调用的ThreadLocal对象的引用)进行查找

1
2
3
4
5
6
7
8
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

首先计算出key在Entry数组中的索引,如果查找到那么直接返回,否则,使用getEntryAfterMiss()方法,依次查找

为什么使用ThreadLocal对象的弱引用作为ThreadLocalMap中Entry的key

弱引用,即对象只存在一个弱引用时,GC时会直接回收

在ThreadLocalMap的Entry中,使用弱引用来关联ThreadLocal对象,这样ThreadLocalMap不会影响ThreadLocal对象的回收,不然,如果使用了强引用,可能在外部调用者释放了ThreadLocal对象的引用后,因为ThreadLocalMap持有一个强引用,ThreadLocal对象将继续存活

但是使用弱引用也带了问题:如果外部调用者释放了ThreadLocal对象的引用,那么接下来的依次GC将回收这一ThreadLocal对象,ThreadLocalMap中Entry的key将指向一个null,但是相应的Entry对象和value将继续存在,引起内存泄漏

为了解决这一问题,在对线程关联的其他ThreadLocal进行get、set时(已被回收threadLocal不存在引用可以调用方法了,所以要等线程关联的其他ThreadLocal调用),会直接或间接的调用expungeStaleEntry()方法,释放key为null的Entry和相应value。但是这种方式也需要等到调用expungeStaleEntry()时才可能释放被回收的ThreadLocal关联的Entry、value

在ThreadLocal类的文档中提到“ThreadLocal instances are typically private static fields”,在将ThreadLocal声明为static变量时,可以从外部更好的控制对ThreadLocal的引用,不会因为作用域等失去引用、导致ThreadLocalMap中的内存泄漏

关于ThreadLocal的子类InheritableThreadLocal

ThreadLocal可以用于在线程生命周期内传递变量,但是在异步任务、子线程等情况下,ThreadLocal不能继续传递。InheritableThreadLocal类即解决这一问题,在创建子线程时,会将父线程持有的InheritableThreadLocal对象传递到子线程

这个过程主要是:在线程init()方法中:

1
2
3
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

其中调用的关键代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}

将父线程的key(即InheritableThreadLocal对象)传入子线程的map作为key,但是value是调用了key(InheritableThreadLocal)对象的childValue()方法来得到一个value,将返回的value作为子线程中这一key对应的值,所以,子线程中持有的值,其实是InheritableThreadLocal中childValue()方法返回的,这一方法默认直接将传入的父线程的v返回,所以默认情况下子线程将和父线程持有同一个value

这种情况下,如果value是一个可变对象(比如Date),那么子线程对它的修改,父线程也是可见的;如果是SimpleDateFormat这样的非线程安全对象,那么子线程、父线程并发使用这一value时会存在线程安全问题

如果重写了InheritableThreadLocal类的childValue()方法,根据父线程的value的值new一个新的对象返回,或者是使用clone()方法(但是clone()方法默认是浅复制),那么子线程将得到一个和父线程值相同的另一个对象实例,自然也不会出现共享实例带来的问题,相当于为每个线程new一个新的对象

所以感觉,InheritableThreadLocal类还是适合传递不可变的对象