本文是对Java ThreadLocal(Java8)的源码的解析,对ThreadLocal基本使用还不了解的朋友可先快速学习ThreadLocal后再来阅读本文。
set方法可以让多个线程保存同一变量的副本。基本使用代码如下:
threadLocal.set(data);
那么为什么ThreadLocal可以起到线程隔离的作用呢?这就要进入set方法源码一探究竟了。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//1
map.set(this, value);
else
//2
createMap(t, value);
}
代码很短,先获取当前的线程,也就是此时调用ThreadLocal的set方法的线程,然后获取ThreadLocalMap,ThreadLocalMap不空就设值,空就创建一个ThreadLocalMap。
ThreadLocalMap是ThreadLocal的静态内部类,从名字也可以看出来它是用于存储ThreadLocal的。跳进getMap方法可以看看。
//ThreadLocal.java
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;
直接返回Thread的ThreadLocalMap,也就是说每个线程都自带一个ThreadLocalMap来存储不同的ThreadLocal。注意,在Java8以前ThreadLocalMap是ThreadLocal成员变量,Java8开始就变了。
线程创建时,它的ThreadLocalMap是空的。也就是说线程第一次调用set方法设值时,会运行代码2创建LocalThreadMap,下次再调用set才会运行代码1。先看看createMap方法做了什么。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
代码很简单就是让当前调用set方法的线程new一个ThreadLocalMap。再看看它的构造方法做了什么。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//1
table = new Entry[INITIAL_CAPACITY];
//2
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
//3
setThreshold(INITIAL_CAPACITY);
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
代码1的table是一个数组,用来保存Entry的,初始容量为16。初始容量的注释上解释道容量必须为2的n次方。接下来进行哈希运算决定新值在数组的插入位置,最后设置一个初始阈值。可以看的出来ThreadLocalMap并是直接用Java自带的HashMap,而是自己实现了Map的相关操作。
先看看代码1的Entry类,它是ThreadLocalMap的静态内部类。其实就是ThreadLocalMap存储的实体,以ThreadLocal为Key,set方法设置的值就是作为Value保存在Entry里的。Entry对ThreadLocal是弱引用,也就是说这个ThreadLocal在下一次Java GC时可能会被回收,采用弱引用的原因会在后面解释。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
先分析一下稍微简单的代码3,它给map设置了一个初始阈值,当map元素个数超过阈值,map就需要扩容了,可以看到这里的阈值就是容量的2/3。
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
回到刚刚的代码2,代码二是用来决定Entry在table数组的插入位置的。
...
//2
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
...
//ThreadLocal.java
private final int threadLocalHashCode = nextHashCode();
插入位置就是ThreadLocal的哈希值和15(INITIAL_CAPACITY 为16)做与运算。threadLocalHashCode是一个常量,由nextHashCode方法得到,看看它的实现。
/**
* 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.
*/
private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode = new AtomicInteger();
...
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
threadLocalHashCode是得到的值其实就是上一次nextHashCode的值加上0x61c88647的结果(因为用的是getAndAdd方法,先得值在自加0x61c88647),为了保证自加是原子操作,所以nextHashCode使用AtomicInteger而非int。HASH_INCREMENT为什么选用0x61c88647可参考这篇文章。
创建了ThreadLocalMap并把值存入,createMap方法就结束了。那么,下一次ThreadLocal再存值时,就运行文章最开始的代码1,回顾一下。
if (map != null)
//1
map.set(this, value);
跳进去看看。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//1
int i = key.threadLocalHashCode & (len-1);
//2
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
//4
e.value = value;
return;
}
if (k == null) {
//5
replaceStaleEntry(key, value, i);
return;
}
}
//6
tab[i] = new Entry(key, value);
int sz = ++size;
//7
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
首先,先在代码1处求得待插入的位置,但这可能并不是最终位置,因为哈希表在插入时有可能出现碰撞的现象(就是说不同的值可能计算出来的插入地址相同),而代码2的for循环就是用来检测插入位置是否冲突的。这个for循环的工作流程是这样的。
如果插入位置是空着的,那没问题直接运行代码6插入就行了。如果插入位置被占用了,那就看看插入位置是否符合要求(代码),插入位置的ThreadLocal和现在调用set方法的ThreadLocal是同一个对象实例,说明本次的调用set方法是在更新副本变量值。
如果比较后发现插入位置的ThreadLocal和本ThreadLocal不是同一个对象实例,这个时候才是真正的碰撞。这里使用了线性检测法来处理冲突。简单的说就是发现插入位置i碰撞,就看看i+1位置是不是空着的,i+1也被占用了就再看看下一个位置行不行,终于找到空位了插入即可。for循环里的nextIndex方法就是在寻找下一个插入位置。如果i已经是数组的最后一个位置了,那就回到第0个位置继续检测。这么一来就总会找到一个空位了。
那么代码5是什么意思,字面上看就是取代旧的Entry。上面说过Entry是ThreadLocal的弱引用,进行一次GC后,Entry所引用的ThreadLocal可能会为空,Entry引用为空又占着table数组的一个空位,这个Entry里保存的副本值也就成了脏数据,此时调用replaceStaleEntry就是在重新利用这个Entry。
再重新读for循环的代码会发现,更新副本值(代码4)和重新利用已有的Entry(代码5)是不会消耗table数组空位的。可代码6新插入的Entry就不一样了,为了保证table数组的空位的到充分利用,每次新插入后都要清理一下脏的Entry及在必要时扩容,看看代码7的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;
//1
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
刚刚提到过Entry引用的ThreadLocal可能会被回收,导致Entry携带脏数据,这个方法就是用来查找并回收这类Entry的。通过do-while循环查找,发现了Entry携带脏数据就调用代码1的expungeStaleEntry方法回收它。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
...
}
把Entry的副本引用设为空,并将它从table数组里移除,接下来等垃圾回收器回收就行了。重新看看代码7的判断。
if (!cleanSomeSlots(i, sz) && sz >= threshold){
refresh();
}
cleanSomeSlots方法里如果找到了携带脏数据的Entry就会返回true,这么一来,只有在出现脏Entry且ThreadLocalMap容量只剩不到1/3时(threshold设定为总容量的2/3)才会调用refresh方法刷新,看看它做了什么。
private void rehash() {
//1
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
//2
if (size >= threshold - threshold / 4)
resize();
}
...
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
现在代码1调用expungeStaleEntries方法做最后一次大回收,如果这次大回收后容量依然还是很低,那就只能调用resize方法扩容了。
expungeStaleEntries之所以叫大回收,是因为它直接把这个table数组遍历了一遍,这样就能最大范围地回收没用的Entry了。
ThreadLocal的get方法基本使用如下:
value = threadLocal.get();
看看源码
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//1
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//2
return setInitialValue();
}
先获取当前线程,在获取这个线程的ThreadLocalMap,代码1获取到存储变量副本的Entry,不空就直接返回变量副本,空就调用代码2的setInitialValue方法设置初值。先来分析代码1的getEntry方法。
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//1
if (e != null && e.get() == key)
return e;
else
//2
return getEntryAfterMiss(key, i, e);
}
代码1先判断ThreadLocal是否匹配,上面讲过,可能出现ThreadLocal可能会被回收导致Entry返回空 又或者 碰撞监测导致插入位置后移,如果匹配就返回,不匹配就调用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)
//1
expungeStaleEntry(i);
else
//2
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
代码1是在重新利用没用的Entry,代码2是在寻找下一个合适的插入位置,上面讲过了。回到刚才getEntry方法,如果连ThreadLocalMap都为空,那么还得对ThreadLocalMap初始化,就是在setInitialValue方法中进行的。
...
//2
return setInitialValue();
}
private T setInitialValue() {
//1
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;
}
代码1的获取副本的初始值,注意,如果不重写initialValue方法,返回值必然是空值。接下来对ThreadLocalMap初始化操作和刚刚讲解的set方法是一样的。如此一来ThreadLocal的get方法就解析完了。
基本使用如下。
threadLocal.remove();
再看看源码。
//ThreadLocal.java
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
//ThreadLocalMap.java
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();
//1
expungeStaleEntry(i);
return;
}
}
}
寻找Entry的方法还是一样的,只是这回是将Entry回收(代码1)。
上面讲到过Entry对ThreadLocal是弱引用,为什么要使用弱引用,而不使用强引用?可以先想想使用强引用会有怎样的后果。
ThreadLocalMap使用了线性检测法来判断处理碰撞。检测到i位置碰撞就看看i+1位置是否能用。问题就出现在这里。如果使用强引用除非手动把table数组的元素置空,否则GC是不会回收这个Entry的,即使Entry所引用的ThreadLocal及对应的变量副本确实没用了。ThreadLocal没用了又不能回收,table数组的元素越来越多,碰撞也越来越严重,如此恶性循环,能用的内存就越来越小了。
这回再想想用弱引用就能明白它的道理了。有一篇文章讨论过要不要使用ThreadLocal的remove方法。个人理解是这样的,ThreadLocalMap采用弱引用策略已经起到回收没用的Entry的作用,只是remove方法让程序更早地解除相关的引用关系,算是一种保障吧。
来看看下面这两个类。
class MyThread extends Thread{
private int mCount = 0;
public int getCount(){
return mCount;
}
public void increase(){
mCount++;
}
}
class MyThread2 extends Thread{
private ThreadLocal<Integer> mCount = new ThreadLocal();
public int getCount(){
return mCount.get();
}
public void increase(){
mCount.set(getCount());
}
}
这两个类对mCount操作的效果是一样的,但使用ThreadLocal和成员私有变量的出发点是不一样的。ThreadLocal是作用在不同的线程空间上的,而私有成员变量是作用在不同的对象实例上的。在MyThread2里 ,想要改变mCount只能在存储它的线程里修改。而在MyThread里,可以在不同的线程里改变同一线程的mCount。
1.为什么使用0x61c88647
2.处理哈希冲突的线性探测法
3.使用ThreadLocal到底需不需要remove?