Java ThreadLocal

一种无锁“共享”方案

既然多个线程同时访问共享数据会有竞争问题,那么每个线程维护单独的数据副本,表面上是访问同一数据,但是实际不共享

Sample
1
2
3
4
5
ThreadLocal<Integer> counter = new ThreadLocal<Integer>();

public void increase(){
counter.set(counter.get() + 1);
}

功能上类似于用线程id为key的map,每个线程独立

Sample
1
Map<Long,Integer> counterMap;

内部实现


虽然在功能上像个Map,但是实现上完全不同
Map对所有键值对进行集中管理,而ThreadLocal是调用的线程各自维护,谁用谁负责

  • 所有的操作都是基于Thread.currentThread(),每个线程都有内部的Map结构ThreadLocal.ThreadLocalMap threadLocals,用于存储线程用到的变量
  • 变量和线程之间是低耦合的,在真正使用时才建立联系。线程提前不知道会使用多少ThreadLocal, ThreadLocal也不知道会有多少线程

ThreadLocalMap


名字里有map,但根本没实现Map接口,是个专有的特殊结构
ThreadLocalMap作为静态内类,只被ThreadLocal使用,所有方法是私有的
底层依然是Entry数组,但是ThreadLocal本身作为key是弱引用实现,即考虑到大数据量的自动释放
如果没有弱引用,即使用户不再使用,整个Entry还在线程map中持有,不会被回收

ThreadLocal$ThreadLocalMap.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static class ThreadLocalMap {
//Entry是弱引用,ThreadLocal作为key值
static class Entry extends WeakReference<ThreadLocal<?>> {
//Entry内维护value值
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; // Default to 0
//...
}

哈希冲突的解决不是链表,而是开放地址
查找时判断是不是直接命中

ThreadLocal$ThreadLocalMap.java
1
2
3
4
5
6
7
8
9
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
//哈希位置Entry存在,并且key就是目标,命中
return e;
else
return getEntryAfterMiss(key, i, e);
}

没命中的情况下,会顺次向后循环查找
值得注意的是如果发现从Entry中获取的key值为null,那么说明已经通过弱引用机制回收了,会把Entry清理出去

ThreadLocal$ThreadLocalMap.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
//已经弱引用回收
if (k == null)
//清理
expungeStaleEntry(i);
else
//获取下一位置
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
// 向后查找,越界后从头开始
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

弱引用被动清理,ThreadLocal作为key变成null,但是Entry中的value还在
自动清理必须是下次访问即getEntry才可能触发,如果长时间不使用,所对应的value还是无法及时释放
特别是搭配线程池使用时,由于线程会重用,不会销毁,造成内存泄漏,最好及时清理
重用也会导致包含遗留数据,注意在开始时及时调用remove清除遗留数据

ThreadLocal$ThreadLocalMap.java
1
2
3
4
5
6
7
8
9
10
11
12
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}

清除逻辑是级联的,不光会清除当前位置,还会重新哈希,过程中可能同时清除其他失效位置
效果是如果释放一个位置,如果原来有哈希冲突的数据,重新哈希后会再次填上释放位置,即如果存在数据那么应当在理论位置,如果位置是null说明数据不存在

ThreadLocal$ThreadLocalMap.java
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
27
28
29
30
31
32
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

//清除当前位置,释放value,数组槽置空
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

//因为是开放地址解决冲突,清除一个数组槽后,还要考虑因为此槽被占用而哈希到其他位置的数据
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//发现失效则清空
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
//发现当前数据的哈希值和理论位置不符,重新哈希
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

ThreadLocal


ThreadLocal内部没有任何数据,只维护了一个哈希值,整体角色是作为一个key存在

ThreadLocal.java
1
2
3
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
}

存值操作

线程的ThreadLocal.ThreadLocalMap threadLocals初始是null,用到时才建立
ThreadLocal只作为key,并不持有value,value是ThreadLocalMap内的Entry持有

ThreadLocal.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//以ThreadLocal自身作为key
map.set(this, value);
else
createMap(t, value);
}

void createMap(Thread t, T firstValue) {
//初始建立ThreadLocalMap
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

取值操作

存在默认值逻辑,即使没有set过,get一样会把ThreadLocal加入map中

ThreadLocal.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}
//默认值逻辑,map不存在或者map存在但是ThreadLocal不存在
return setInitialValue();
}

初始化操作

ThreadLocal有一个提供初始值的initialValue函数,默认是null,子类可以重写
当线程调用get没找到值时就会调用该函数,包含两种情况

  • 线程第一次调用get并且之前没被set
  • 线程调用remove清空后再调用get
ThreadLocal.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
//重写自定义默认值
protected T initialValue() {
return null;
}

只有key不存在时才会调用默认值方法

Sample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ThreadLocal<String> tl = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "aaa";
}
};
//整个ThreadLocalMap不存在
System.out.println(tl.get()); //aaa

//key存在,只不过Entry里的value是null
tl.set(null);
System.out.println(tl.get()); //null

//key不存在
tl.remove();
System.out.println(tl.get()); //aaa

清理操作

获取当前线程上的map, 从中删除自己

ThreadLocal.java
1
2
3
4
5
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

InheritableThreadLocal


ThreadLocal还有一个子类,可以用于传递给子线程
和普通的ThreadLocal不同的是,在Thread类中保存的位置不同,这个位置的数据会复制给子线程

InheritableThreadLocal.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class InheritableThreadLocal<T> extends ThreadLocal<T> {

protected T childValue(T parentValue) {
return parentValue;
}

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

void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

JDK使用


java.util.concurrent.ThreadLocalRandom
Random本身是线程安全的,ThreadLocal版本可以避免多线程下对随机seed的争抢,提升性能

应用


SimpleDateFormat线程不安全,过程中通过设值给内部Calendar,然后进行操作。多线程下一起操作Calendar状态不可控,可能产出和预期不一样的结果
解决方案是可以用ThreadLocal包装

Sample
1
2
3
4
5
6
private static final ThreadLocal<DateFormat> FORMAT = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
}

Java8后可以使用线程安全的DateTimeFormatter