Java多线程基础 06.深入理解ThreadLocal
ThreadLocal的使用
ThreadLocal是一个存放线程本地变量的工具类。
ThreadLocal的线程本地性质
示例代码 1.1
public class ThreadLocalTest {
public static void main(String[] args) throws InterruptedException {
ThreadLocal<Integer> stringThreadLocal = new ThreadLocal<>();
new Thread(() -> {
stringThreadLocal.set(1);
while (true) {
}
}).start();
Thread.sleep(1000);
new Thread(() -> {
Integer integer = stringThreadLocal.get();
System.out.println(integer);
}).start();
}
}
输出:
null
ThreadLocal设置初始值
示例代码 1.2
public class ThreadLocalWithInitialTest {
public static void main(String[] args) throws InterruptedException {
ThreadLocal<Integer> stringThreadLocal = ThreadLocal.withInitial(() -> 99);
System.out.println(stringThreadLocal.get());
}
}
输出:
99
ThreadLocal的原理
Thread中有一个类型为ThreadLocal.ThreadLocalMap的threadLocals属性(见示例源码 2.1),先可以把它理解为key为ThreadLocal,value为Object的Map。
源码 2.1
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocal.get方法
如源码2.2所示,当调用ThreadLocal实例的get方法时,会先获取从当前线程的threadLocals属性中获取。如果没找到,会进行初始化操作,最终返回设置的初始化值。
源码2.2 ThreadLocal代码片段
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;
}
}
//如果Thread.currentThread().threadLocals中找不到,则进行初始化。
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
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;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal.set方法
如源码2.3所示,当调用ThreadLocal实例的set方法时,会先判断当前线程的threadLocals是否已经初始化完成。如果threadLocals未初始化完成,会进行初始化;如果threadLocals已经初始化完成便会调用set方法将其写入threadLocals中。
源码2.3 ThreadLocal代码片段
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.remove方法
如源码2.4所示,ThreadLocal中的remove方法逻辑很简单,获取当前线程的threadLocals对象,并调用其remove方法。
源码2.4 ThreadLocal代码片段
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
}
ThreadLocalMap初始化
如源码 3.1.1所示,ThreadLocalMap初始化时,就将table初始化为长度16的数组,并基于初始长度计算出阈值threshold。
源码3.1.1 ThreadLocalMap代码片段
static class ThreadLocalMap {
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
/**
* 将阈值设置为 (当前数组长度 * 2)/ 3。
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
}
ThreadLocalMap.get方法
在讲ThreadLocalMap的get方法之前,需要先讲讲ThreadLocalMap中对哈希冲突的解决策略。
在散列表面对哈希冲突问题时通常有两种解决方案,链地址法和开地址法,除此之外还有再散列法和建立公共溢出区。链地址法就是将哈希表中每一个单元都设置为链表,Java中的HashMap就使用的链地址法。而ThreadLocalMap中使用的是开放地址法中的线性探测法,就是如果发现哈希冲突,就把元素放在下一个单元。
在了解ThreadLocalMap的哈希冲突解决策略之后,就可以很容易看懂get方法了,具体逻辑见源码3.2.1的注释。
源码3.2.1 ThreadLocalMap.getEntry
private Entry getEntry(ThreadLocal<?> key) {
// 计算在散列表中的索引
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 如果当前索引下不为空,同时e中的引用和key相同。
if (e != null && e.get() == key)
// 返回entry实例
return e;
// 有两种情况会走到else语句:
// 1:table[i]为空
// 2:table[i]不为空,但key不同,需要基于线性探测法进行查找。
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//判断是否循环到了线性探测法边界
while (e != null) {
ThreadLocal<?> k = e.get();
// 当前entry中的引用和key相同,返回entry。
if (k == key)
return e;
//判断当前slot中的entry#key关联的ThreadLocal对象是否已被GC回收了。
if (k == null)
//探测过期数据,回收slot。
expungeStaleEntry(i);
else
//更新index,继续向后搜索。
i = nextIndex(i, len);
//获取下一个slot中的entry。
e = tab[i];
}
return null;
}
private static int nextIndex(int i, int len) {
// 实现一个环形的访问
return ((i + 1 < len) ? i + 1 : 0);
}
ThreadLocalMap.expungeStaleEntry方法
源码3.3.1 ThreadLocalMap.expungeStaleEntry方法
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清空当前过期slot,方便gc。
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
// rehash直到遇到null
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果当前slot里面内容过期了,再做一次清空
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//没有过期,会进else语句
int h = k.threadLocalHashCode & (len - 1);
//如果当前元素因为hash冲突,所以进行一次rehash。
if (h != i) {
//清空当前slot
tab[i] = null;
//再基于线性探测法查找slot,存在entry。
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
ThreadLocalMap.remove方法
如源码 3.4.1所示,ThreadLocalMap中的remove方法会基于线性探测法,依次循环。如果找到对应entry,便清空entry中引用,并触发一次过期数据探测。
expungeStaleEntry方法流程见上。
源码3.4.1 ThreadLocalMap.remove
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;
}
}
}
ThreadLocalMap.set方法
源码3.5.1 ThreadLocalMap中set相关代码
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算哈希表中的节点槽位
int i = key.threadLocalHashCode & (len-1);
//这个for循环,就是对开地址法的实现。
//如果当前槽位不为空,就会一直往后找空位。
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果发现Entry已存在,那就直接替换Entry中的value。
if (k == key) {
e.value = value;
return;
}
// 如果发现过期的slot
if (k == null) {
//替换过期数据的逻辑。
replaceStaleEntry(key, value, i);
return;
}
}
//什么时候会执行到这边?
//直到循环到空slot,仍然没有找到对应的entry,同时也未出现过期entry
//往空位中设置Entry。
tab[i] = new Entry(key, value);
int sz = ++size;
//做一次启发式清理
//条件一:!cleanSomeSlots(i, sz) 成立,说明启发式清理工作 未清理到任何数据..
//条件二:sz >= threshold 成立,说明当前table内的entry已经达到扩容阈值了..会触发rehash操作。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private void rehash() {
//这个方法执行完后,当前散列表内的所有过期的数据,都会被清理。
expungeStaleEntries();
//清理完过期数据后,如果当前散列表内的entry数量仍然达到了 threshold * 3/4,触发扩容。
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);
}
}
ThreadLocalMap.replaceStaleEntry方法
当ThreadLocalMap.set方法中,线性探测到过期entry,便会进入当前方法。
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//表示 开始探测式清理过期数据的 开始下标。默认从当前 staleSlot开始。
int slotToExpunge = staleSlot;
//向前探测过期数据,找到空节点之后最前面的过期数据。
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len)){
if (e.get() == null){
slotToExpunge = i;
}
}
//从staleSlot往后去查找,直到null为止。
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
//向后查询的过程中,找到了对应的entry。
//进行替换操作
e.value = value;
//进行交换操作
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//条件成立:
// 1.向前检查过期数据过程中未发现过期的entry。
// 2.向后检查过期数据过程中也未发现过期的entry
if (slotToExpunge == staleSlot)
//开始探测式清理过期数据的下标 修改为 当前循环的index。
slotToExpunge = i;
//cleanSomeSlots :启发式清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//条件1:k == null 成立,说明当前遍历的entry是一个过期数据..
//条件2:slotToExpunge == staleSlot 成立,一开始时 的向前查找过期数据 并未找到过期的entry.
if (k == null && slotToExpunge == staleSlot)
//因为向后查询过程中查找到一个过期数据了,更新slotToExpunge 为 当前位置。
//前提条件是 前驱扫描时 未发现 过期数据..
slotToExpunge = i;
}
//什么时候执行到这里呢?
//向后查找过程中 并未发现 k == key 的entry
//直接将新数据添加到 table[staleSlot] 对应的slot中。
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//条件成立:除了当前staleSlot 以外 ,还发现其它的过期slot了.. 所以要开启 清理数据的逻辑..
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
ThreadLocalMap.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];
//条件一:e != null 成立
//条件二:e.get() == null 成立,说明当前slot中保存的entry 是一个过期的数据..
if (e != null && e.get() == null) {
//重新更新n为 table数组长度
n = len;
/表示清理过数据.
removed = true;
//以当前过期的slot为开始节点 做一次 探测式清理工作
i = expungeStaleEntry(i);
}
// 假设table长度为16
// 16 >>> 1 ==> 8
// 8 >>> 1 ==> 4
// 4 >>> 1 ==> 2
// 2 >>> 1 ==> 1
// 1 >>> 1 ==> 0
} while ( (n >>>= 1) != 0);
return removed;
}
ThreadLocalMap.resize方法
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//计算出扩容后的新表大小
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
//表示新table中的entry数量。
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
//清除当前Entry,帮助GC回收。
e.value = null;
} else {
//当前Entry是非过期数据,需要迁移到扩容后的新表。
//计算出当前entry在扩容后的新表的 存储位置。
int h = k.threadLocalHashCode & (newLen - 1);
//while循环 就是拿到一个距离h最近的一个可以使用的slot。
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}