Java并发编程之ThreadLocal源码分析

1 一句话概括ThreadLocal

  什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象的线程创建了一个独立的变量副本。

2 ThreadLocal使用场景

  用一句话总结ThreadLocal真的实在是太苍白无力了!我们通过一个简单的例子入手。比如现在有A和B两台服务器需要通过http请求传递数据,但又希望数据安全性有一定保障,因此发送方A决定用AES算法对传输数据加密后再发送给B。接受方B收到数据后,通过密钥解密数据并进行后续的业务处理。
  对数据进行AES解密,接收方B可选择JAVA提供的Cipher类来实现。我们在调用Cipher类进行解密时时,需要获取Cipher对象的实例,如下所示:

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");

  接着我们调用该实例就可以进行数据解密工作。但很不幸的是,Cipher类存在线程安全问题,它无法工作于多线程场景下。简单来说就是单个Cipher实例无法同时解密多条数据。
  那怎么办呢?
有没有什么办法能让每个线程拥有相同的instance实例,且彼此互不干扰呢?这个时候我们可以借助ThreadLocal类来实现特定功能,ThreadLocal能够为每个使用该对象的线程创建独立的变量的副本。这样就满足了我们的需求。

  后续篇幅我们会深入到ThreadLocal的源码层来探讨它的实现机制,在看ThreadLocal类的几个基本方法前,让我们先看一下ThreadLocal中静态类ThreadLocalMap的实现,它对于我们理解ThreadLocal有着举足轻重的作用!

3 ThreadLocalMap源码分析

  ThreadLocalMap是ThreadLocal类中的一个静态类,它拥有一个Entry数组类型的成员变量,名为table。这个Entry是啥?我们来看看。

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

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

  Entry类是ThreadLocalMap中的一个静态类,它继承了WeakReference类,同时拥有一个类型为Object的value成员变量。当我们创建Entry对象后,调用Entry.get()方法,获取到的实际上是ThreadLocal对象的弱引用。而这个设计则保证了Entry对象中保存的ThreadLocal弱引用是易被回收的。网上有很多关于ThreadLocal对象是否会引发内存泄漏的文章,这里的内存泄漏通常指的不是entry的key,也就是ThreadLocal的弱引用。而是这里的value对象。实际上ThreadLocalMap自身提供了一套回收无用Entry节点的机制。在后面我们会聊到它的实现。关于ThreadLocal是否会引发内存泄漏,这里暂时不做探讨。

3.1 ThreadLocalMap.set()方法

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();
}

  ThreadLocalMap类持有一个Entry数组,名为table。当我们调用ThreadLocalMap的set()方法时,其实就是更新table中某个Entry,或往table中插入一个Entry。set()方法其实是根据threadLocal对象的threadLocalHashCode来计算当前Entry节点应落入什么位置。当然存在多个entry落入位置发生冲突的情况,在ThreadLocal中使用了线性探测法来解决冲突。知道了这一点,那set方法的实现思路就很清晰了。就是找一个位置让我放节点嘛!如果已经有现成的了,更新一下value就行。要是找不到,那我就按线性探测法来找落入位置就好了嘛。值得注意的是replaceStaleEntry()方法!当entry节点不为空,而key为null时会调用这个方法。这个方法看名字好像是用来替换过时的Entry节点的?我们来看一下它到底是干嘛的?

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;

// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;

// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();

// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;

tab[i] = tab[staleSlot];
tab[staleSlot] = e;

// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}

// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}

// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);

// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

  这个方法好像还挺长的,上面我们已经猜测过它的用途了,现在我们就来揭开它的庐山真面目。
  首先关注一个这个方法的参数:

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot)

  第一个参数就是我们的key,估摸着还是用来计算hash值找位置的;第二个就是要放的value了;第三个staleSlot呢?我们上面好像是找到了一个key为null的entry节点吧?没错,这个staleSlot就是这个节点在tab中的位置了。然后从这个staleSlot节点开始往前找,如果发现某个entry不为空,但key等于null,用slotToExpunge记录下它的位置,直到往前找到一个entry为null的节点停止。这个slotToExpunge是用来干嘛的呢?后面会提到。
  我们接着看。往前找完之后,我们又从staleSlot的下一个节点开始往后找,如果发现了某个节点的键值等于我们的key。我们是不是应该用我们的value替换掉这个位置原先的值呢?好像是应该替换。但是别忘记了前面还有个key为null的entry节点呢!由于之前key为null的节点和当前节点计算出来hash值其实是一样的。这里我们将e节点的值更新为最新的value后,互换tab[i]和entry的位置。这一步的目的是什么呢?我猜大概是这样的,因为ThreadLocalMap是根据线性探测法来解决冲突的,因此可能会出现key的哈希值相同但散落位置不连续的情况。为了在一定程度上提高查找哈希值相同entry节点的效率,交换一下位置会是更好的选择。同时接下来会执行cleanSomeSlots()方法。我们上面的for循环会一直往后找,直到发现一个null节点为止。如果找到了null节点,那就说明按照线性探测法找不到这个节点了啊!那咋办呢?staleSlot节点不是空着呢么。直接塞进去不就完事了。。
  最后一句又调用了cleanSomeSlots()方法。下面就轮到它了。。

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;
}

  在看cleanSomeSlots()方法前,还得看expungeStaleEntry()。有时候不得不说,好的java命名规范,真的是很重要啊!看到这个方法的名字就知道它大概是用来清理过期节点的。回想一下,有什么节点是需要我们的清理的吗?好像有。。前面是不是有找到过key为null的entry节点啊?这个key为null的节点好像没啥用啊!

cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

  还记得这个slotToExpunge吗?不记得了的话往前翻一翻。这个slotToExpunge位置指向了一个key为null的entry节点。既然知道这个节点是没用的,那它就应该被回收。这里就很直接粗暴了,直接把它直接null以待后面垃圾回收器清理。清理完之后,又是一个for循环。如果key为null,将该entry置为null。如果不为null,重新计算一下hash值,如果位置与当前位置不同,需要重新找一个位置放该节点。当然也是利用线性探测法了,找到连续位置后面第一个为null的节点放置。
  最后返回的节点为从slotToExpunge往后的第一个值为null的entry节点。
  再看看cleanSomeSlots方法主体。

private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}

  这个方法顾名思义是用来清理某些节点的。清理啥节点呢?还是那些不为null但是key为null的节点。参数n决定了for循环要执行的次数。>>>在java中是无符号位移的意思,也就是说如果每次循环tab[i]均不需要清理,最多会执行logn次。如果有需要清理的节点,就会调用expungeStaleEntry()方法去回收这个节点。
  上面说了这么一大堆,终于把ThreadLocalMap的set()方法说完了。下面接着来看getEntry()方法。

3.2 ThreadLocalMap.getEntry()方法

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);
}

  getEntry()方法比较简单。先根据key值计算出对应在table中的位置,如果table[i]的key值和我们的参数key相同,直接返回table[i];反之,调用getEntryAfterMiss()方法。

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;
}

  进入到这个方法的情况可能如下三种:

  1. entry节点为null
    如果entry节点为null,直接返回null。
  2. entry节点不为null,但是key为null
    执行expungeStaleEntry()方法回收该节点,回收完之后,tab[i]节点也就变成了null,直接返回null。
  3. entry节点不为null,entry节点的key和参数key不同
    从i节点开始往后找,如果有key值相同的节点,也就是我们找到了我们需要的节点,返回entry即可。如果找不到,从i节点往后找,遇到key为null的回收一下该节点后返回null,遇到entry为null的直接返回null;

  getEntry()方法会被ThreadLocal的get()方法调用,我们会在稍后的ThreadLocal源码的讲解中再谈。
  介绍ThreadLocalMap用了不少的篇幅啊!下面就来看看我们ThreadLocal啦!关于ThreadLocal的方法网上已经有太多太多的文章介绍了。不过这里我们还是简单的结合我们上面所说的ThreadLocalMap来总结一下!

4 ThreadLocal源码分析

4.1 ThreadLocal.Set()方法

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对象的set方法时,程序会获取当前线程,并将其作为作为参数传递给getMap()方法。

Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.java
ThreadLocal.ThreadLocalMap threadLocals = null;

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

  查看Thread类源码可以发现,Thread类中包含了一个ThreadLocalMap对象,这个家伙我们上面已经花了很大的篇幅来说了,简单的说它的key为ThreadLocal的弱引用,而value为待保存的对象。至于为什么是弱引用,大家自己去google下。
  继续说getMap()方法。getMap()方法很简单:返回当前线程中的ThreadLocalMap对象。

  • 获取到ThreadLocalMap对象后,如果它不为空。则往该对象里面塞入一个键值对,key为ThreadLocal对象的弱引用,vaule为需要保存的对象。
  • 如果ThreadLocalMap对象为null,则调用createMap()方法。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

  createMap()方法在这里不细说。它会初始化当前线程的ThreadLocalMap对象,并将当前需要保存的对象放入ThreadLocalMap中,key值为当前线程的弱引用对象。
  小结一下:ThreadLocal的set方法会获取当前线程的ThreadLocalMap对象,如果TreadLocalMap对象不为空,则将当前线程的弱引用作用key,待保存对象作为value保存起来;若ThreadLocalMap对象为null,则会先初始化,再放入键值对。

4.2 ThreadLocal.Get()方法

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}

  知道了怎么放,接下来聊一聊怎么取。眼尖的朋友们肯定已经发现了,这里又出现了ThreadLocalMap对象。那是自然,因为我们上面不就是往ThreadLocalMap里面放的吗!还记得ThreadLocalMap里面存了啥不?不记得的往上翻一翻。
  如果ThreadLocalMap对象不为空,当前线程作为key值,从ThreadLocalMap中取出来了一个ThreadLocalMap.Entry对象。这个getEntry()方法我们在上面已经已经介绍过了,可能再返回去看看。当然了,肯定有人要问!我们刚才放的时候放的明明不是ThreadLocalMap.Entry对象!这咋回事呢?

ThreadLocalMap.java
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;

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

  实际上,在ThreadLocalMap中有一个静态类,它名叫Entry,继承了WeakReference类。再看看Entry的构造方法。如果调用Entry的get方法,实际上拿到的是ThreadLocal对象的弱引用对象。是不是很熟悉?上面的set方法有聊到过。
  继续说上面的get()方法。当我们拿到了Entry对象后,如果Entry对象不为空,直接返回Entry对象的value值,即我们想要的值。
那么如果ThreadLocalMap为空呢?则会执行setInitialValue()方法。光看名字,你肯定觉得它无非执行了两步操作:1.初始化对象;2. 将初始化后的对象塞入ThreadLocalMap对象中;3. 返回初始化后的对象。那我们来看看我们的猜想对不对呢?

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;
}

  除了第一句代码,后面的是不是好像都在哪里见过啊?可不是!不就是上面的set()方法吗?这个方法我们只需要关注initialValue()方法...而这个initialValue()方法是需要我们自己重写的。

  小结一下:如果ThreadLocalMap中有ThreadLocal对应的值,取Entry对象的value值;如果ThreadLocalMap为null,三步走!初始化,将初始化后的对象放入ThreadLocalMap中,返回初始化后的对象。

5 ThreadLocal总结

  很感谢您能耐心的看到这里,我们最后再总结一下ThreadLoacl的实现机制。
  在Thread类中存在一个ThreaLocalMap变量,ThreadLocalMap中又有一个Entry类型的数组,而这个Entry对象则以ThreadLocal的弱引用为key。当我们调用ThreadLocal的get()方法时,会先获取当前线程的ThreadLocalMap对象,并将当前ThreadLocal对象作为key(实际上key为ThreadLocal的弱引用),去它的Entry数组中寻找我们需要的value。就这是我们说ThreadLocal为每个线程创建了一个变量副本的意思,线程对自己ThreadLocalMap中的值进行操作时,并不会对其它线程造成影响。

posted @ 2018-01-01 11:28  使徒行者白  阅读(1136)  评论(0编辑  收藏  举报