基于 JDK1.8
ThreadLocal
ThreadLocal
可以用于创建线程本地变量,每个线程使用 ThreadLocal
都会拥有自己对该变量的副本,即每个线程都可以独立地改变其副本的值,而不会影响其他线程的副本。
数据结构
ThreadLocal
其实就是由内部实现的 ThreadLocalMap
组成的,ThreadLocalMap
是以 ThreadLocal
的弱引用作为 key
,value
为代码中放入的值,每个线程在往 ThreadLocal
里放值的时候,都会往自己的 ThreadLocalMap
里存,读也是以 ThreadLocal
作为引用,在自己的 map
里找对应的 key
,从而实现了线程隔离。
ThreadLocalMap
数据结构
ThreadLocalMap 底层就是由 Entry 数组构成
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap.set() 原理解析
源码
private void set(ThreadLocal<?> key, Object value) {
// Entry数组
Entry[] tab = table;
int len = tab.length;
// 获取当前key 的索引位置
int i = key.threadLocalHashCode & (len-1);
// 以当前 key 对应的 Entry 为起点,遍历 Entry 数组
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 当前 Entry 的 key 等于插入值的 key
if (k == key) {
e.value = value;
return;
}
// 当前 Entry 的 key 为 null
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 插入元素
tab[i] = new Entry(key, value);
int sz = ++size;
// 进行启发式清理工作,清除 key 为 null 的数据
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
-
将
key
的HashCode
与Entry
数组的长度减一相与获取key
对应的索引位置 -
根据索引位置获取
key
对应的Entry
,以该Entry
为起点遍历Entry
数组- 如果当前
Entry
为null
,那么直接创建一个新的Entry
插入 - 如果当前的
Entry
不为null
- 如果当前
Entry
的key
等于插入值的key
,那么就会替换当前Entry
的value
,然后结束方法。 - 如果当前
Entry
的key
为null
,说明Entry
是过期数据,执行replaceStaleEntry
方法来替换过期的数据,然后结束方法。 - 如果以上两种情况都不符合那么就会获取到下一个
Entry
继续判断
- 如果当前
- 如果当前
-
增加
Entry
数组的长度 -
调用
cleanSomeSlots
方法进行启发式清理工作,清理数组中Entry
的 key 为null
的数据,如果cleanSomeSlots
方法未清理任何数据,且当前数组的长度超过了阈值,那么就会调用rehash
方法进行扩容
ThreadLocalMap.replaceStaleEntry() 原理解析
源码
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 待插入元素对应的索引位置
int slotToExpunge = staleSlot;
// 从待插入元素对应的索引位置开始向前迭代,
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 从待插入元素的索引位置 staleSlot 开始向后迭代
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// key 相同
if (k == key) {
e.value = value;
// 互换位置
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 清除过期 Entry
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// key 为null
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 未找到 key相同的Entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 清理过期数据
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
- 从插入值的索引位置
staleSlot
开始向前迭代,找到其他过期数据,然后更新过期数据的起始位置slotToExpunge
,直到Entry
为null
。 - 从待插入元素的索引位置
staleSlot
开始向后迭代- 如果找到了
key
相同的Entry
,则将当前Entry
的值设置为我们插入的值,然后将当前Entry
的位置与待插入元素对应的Entry
位置互换,接着就根据第 1 步获取到的slotToExpunge
开始过期Entry
的处理,最后退出方法。 - 如果当前
Entry
的key
为null
,且过期数据的起始位置slotToExpunge
位于staleSlot
,那么更新slotToExpunge
为当前Entry
的索引位置。
- 如果找到了
- 如果在循环中找不到
key
相同的Entry
,就会创建新的Entry替 换table[stableSlot]
的位置 - 最后进行过期数据的清理
ThreadLocalMap.expungeStaleEntry() 原理解析
int staleSlot
:空 slot 的起始位置
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// staleSlot 即为过期 Entry 的起始位置
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();
// key 为null
if (k == null) {
e.value = null;
tab[i] = null;
size--;
// key 不为 null
} 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;
}
- 将起始位置的
Entry
设置为null
,并减小数组中元素的个数。 - 从起始位置开始向后遍历
Entry
数组:- 如果遇到
key
为null
的Entry
,那么就把当前的Entry
设置为null
,并且减小数组中元素的个数,然后继续往后探测。 - 如果碰到
key
不为空的Entry
(记作 e ),那么就会重新计算当前key
对应的索引位置(记作 h ),如果与原先的索引位置(也就是当前的 i 值)发生偏离,那么就会将i
对应的Entry
设置为null
,然后重新计算e
的位置。 - 如果碰到空的
slot
则结束清理,返回空slot
的位置。
- 如果遇到
ThreadLocalMap.cleanSomeSlots() 原理解析
int i
:当前 Entry 的位置
int n
:在插入元素时调用,这个参数代表元素的个数;在 replaceStaleEntry 中调用,这个参数代表数组的长度 。
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;
}
- 从当前 Entry 开始向后遍历,遍历 log2(n) 个元素
- 遇到 key 为null 的Entry 则调用 expungeStaleEntry 进行清理
ThreadLocalMap 扩容机制
在set方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值就会执行rehash()
rehash 方法
private void rehash() {
expungeStaleEntries();
// 数组中的元素个数是否大于 threshold * 3/4
if (size >= threshold - threshold / 4)
resize();
}
- 首先会调用
expungeStaleEntries
方法对Entry
数组进行过期数据的清理 - 然后判断当前数组中的元素个数是否大于 threshold * 3/4 ,如果大于那么就会调用 resize 方法进行扩容
resize 方法
private void resize() {
Entry[] oldTab = table;
// 旧容量
int oldLen = oldTab.length;
// 新容量
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (Entry e : oldTab) {
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
// 重新计算hash
int h = k.threadLocalHashCode & (newLen - 1);
// hash 冲突则往后查找
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
// 重新计算扩容阈值
setThreshold(newLen);
size = count;
table = newTab;
}
- 扩容后的
Entry
数组的长度变为旧容量的2
倍 - 然后去遍历旧的
Entry
数组,重新计算hash
位置,然后放到新的Entry
数组中,如果出现hash
冲突则往后寻找最近的Entry
为null
的槽位,遍历完成之后,旧数组中所有的Entry
数据都已经放入到新的Entry
数组中。最后重新计算数组的扩容阈值。
ThreadLocalMap.getEntry() 原理解析
ThreadLocal<?> key
:当前key
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);
}
根据 key
的 hash
获取对应的 Entry
,如果当前 Entry
的 key
等于查找的 key
那就返回当前 Entry
,否则就调用 getEntryAfterMiss
方法。
ThreadLocalMap.getEntryAfterMiss() 原理解析
当 key
在其直接哈希槽中找不到时使用
ThreadLocal<?> key
: ThreadLocal 对象
int i
: ThreadLocal 对应的 hash 值
Entry e
: ThreadLocal 对应的 Entry
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;
}
- 如果当前
key
对应的Entry
不为空- 判断当前
Entry
的key
是否等于获取的key
,是的话那么就返回当前的Entry
。 - 如果当前
Entry
的key
为null
那么就会调用expungeStaleEntry
方法来清理,接着继续往后迭代。
- 判断当前
- 如果当前
key
对应的Entry
为空就直接返回null