基于 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