ThreadLocal的使用及原理

概要

如果你还不知道threadlocal,那你就要了解一下,相信你一定会用到它。

作用

threadlocal最大作用就是提供线程级别的变量生命周期。
试想,如果你需要一个变量在一个线程的生命周期内都可以访问到,在不使用threadlocal的前提下你会怎么做?你或许这样做

  1. 提供一个类级别或者静态变量
    但是这个方法大家很容易就想到在高并发时会出问题。

  2. 把这个局部变量一直传递下去
    但是如果你要调用的方法层次很深呢?难道你对每个方法都增加一个参数吗?显然不实际。

所以threadlocal就是提供了一个可行的方案,使得这个变量可以随时访问到,并且不会跟其他线程产生冲突。

使用

threadlocal的使用很简单,就是一个get, set。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ThreadLocalTest {

   //定义一个ThreadLocal的变量, 需要指定类型
   public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

@Before
public void init(){
  #set值进去
       threadLocal.set("test");
}

@Test
public void test() {
  //在需要时get出来
       System.out.println("threadLocal's value=" + threadLocal.get());
}

}

实现原理

set

我们先从set方法入手看看做了手脚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void set(T value) {
  //取出当前线程
      Thread t = Thread.currentThread();
       //根据当前线程获取ThreadLocalMap。从getMap方法可以看到这个ThreadLocalMap就是保存在Thread对象里面
       ThreadLocalMap map = getMap(t);
       if (map != null
#map已经存在就是set进去
map.set(this, value);
       else
#不存在新建一个map
createMap(t, value);
}

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

   //createMap方法
   //注意,这里的this指的是ThreadLocal对象
   void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

我们再看看ThreadLocalMap的创建及其他方法。ThreadLocalMap是定义在ThreadLocal里的一个静态类。

1
2
3
4
5
6
7
8
9
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
           //通过ThreadLocal的hashCode确定index,这个我们稍后再说
           int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
           //重点,把ThreadLocal对象作为key存到了ThreadLocalMap的Entry里
           table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

set方法比较简单,跟普通的Map差不多,把key和value set进去。
里面还包含了清理key为null的Entry对象的一些操作。

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 void set(ThreadLocal<?> key, Object value) {

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

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)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public T get() {
//取当前线程
       Thread t = Thread.currentThread();
       //取出线程的ThreadLocalMap,跟上面是一样的
       ThreadLocalMap map = getMap(t);
if (map != null) {
      //根据当前对象ThreadLocal取出ThreadLocalMap.Entry
           ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
       //如果map为null则做初始化,跟上面的createMap差不多
       return setInitialValue();
}

图解关系

upload successful

可以看出相同的ThreadLocal在不同的线程有不同的值。

主要记住ThreadLocal是作为ThreadLocalMap的key,可能开始有点绕,但是慢慢思考,理清它们的关系就行了。

两个问题

内存泄漏 ?

这是一个对ThreadLocal来说老生常谈的问题了。那使用ThreadLocal为什么会导致内存泄漏?还有我们应该怎么去避免?是我们应该关注的两个点。

原因

首先,我这里假设大家对java的内存回收机制和引用(Reference)有一定的了解。如果不知道,请自行google了。

我们先看看ThreadLocalMap的Entry的定义

1
2
3
4
5
6
7
8
9
//对key使用了WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
           Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

为什么要使用WeakReference?
网上很多的说法都说是使用了弱引用就会被GC一句带过,我觉得很多都说得不清不楚的。我通过自己的理解向大家解析一下:
其实很简单,从变量的作用域及引用关系的角度出发思考。试想如果一个ThreadLocal定义为一个类实例的变量或者是一个方法内的局部变量,那么当这个类实例被销毁了或方法退出了,在理想的情况下,垃圾回收器应该回收掉这个ThreadLocal是吧,毕竟它的生命周期已经完结了,但是如果这时ThreadLocalMap还是持有这个ThreadLocal的强引用的话,这个ThreadLocal就不会被回收,直到这个ThreadLocalMap被销毁或者这个线程被销毁。
说白了,从上面那个图看出,这样的设计导致的结果是这个ThreadLocal的生命周期跟线程的生命周期挂上钩了。

同时,这里又会出现另外一种内存泄漏的问题,即使ThreadLocal回收了,但是value没有被回收,还是会导致内存泄漏。但是你没办法把value设置为WeakReference,因为value不是你的,不归你管。

如何防止

ThreadLocal采用如下解决内存泄漏,看expungeStaleEntry方法。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

// Rehash until we encounter null
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;

// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

在ThreadLocal的源码你会看到很多清洗数据的代码最终都会调用到这个方法。这个方法主要逻辑,简单来说就是当key为null,把value也设置为null,从而让value也被回收。
这个方法的触发点有很多,当对ThreadLocal进行set,get,remove等操作时都会。

容器(如tomcat,netty)一般都是使用线程池处理用户到请求,此时用ThreadLocal要特别注意内存泄漏的问题,一个请求结束了,处理它的线程也结束,但此时这个线程并没有死掉,它只是归还到了线程池中,这时候应该清理掉属于它的ThreadLocal信息。

所以我们使用ThreadLocal一个比较好的习惯是在finally块调用remove方法。

hashcode和0x61c88647?

既然ThreadLocal用map就避免不了冲突的产生。

在ThreadLocalMap的构造方法中,我们可以看到以下代码

1
2
3
//table的下标的计算方式
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//threadLocalHashCode的定义
private final int threadLocalHashCode = nextHashCode();

private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
   //HASH_INCREMENT的十进制为1640531527
   private static final int HASH_INCREMENT = 0x61c88647;

从代码可以看出每个ThreadLocal的实例的threadLocalHashCode的差值为0x61c88647这么多,那为什么要这样做呢?

这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。
斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说
(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647。
通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。
ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。。为了优化效率。

简单来说就是在table[]的size为2的次幂情况下,取模会得到均匀分布。

一个优化点

从上面得知,ThreadLocal的Map可能会产生冲突,解决冲突的办法是线性探测。
而Netty的FastThreadLocal的利用了一个自增序号来作为下标,避免了冲突的产生。

1
2
3
4
5
6
7
8
9
10
11
12
public FastThreadLocal() {
index = InternalThreadLocalMap.nextVariableIndex();
}

public static int nextVariableIndex() {
int index = nextIndex.getAndIncrement();
if (index < 0) {
nextIndex.decrementAndGet();
throw new IllegalStateException("too many thread-local indexed variables");
}
return index;
}

参考资料

https://juejin.im/post/5b5ecf9de51d45190a434308