JDK源码分析实战系列-ThreadLocal
总览
ThreadLocal提供了线程局部变量的解决方案。
我们知道成员变量在多线程下是不安全的,而局部变量的生命周期又取决于变量定义的范围。那么有没有一个变量定义可以专属于各自线程,生命周期由线程控制,并且和其他线程的变量完全隔离,确保线程安全。简单想到一个解决办法,定义一个这样的结构:Map<Thread, Map<Object, Object>>,Thread为Key,一个Map为value,这个Map里存着这个线程的独有变量,只需要保证访问变量的时候都是通过Thread来检索的,可以从表面上做到线程隔离的效果。结构类似如下下图:
带着这个结构思路,再继续仔细研究一下ThreadLocal是如何实现的。
从ThreadLocal public method中总览一下它在实现,除了构造方法,以下为关键方法:
set(T value)
,往ThreadLocalMap
放值,key是ThreadLocal本身get()
,从ThreadLocalMap
以ThreadLocal本身为key获取Map中的valueremove()
,从ThreadLocalMap
以ThreadLocal本身为key删除这个键值对
发现所谓的线程局部变量实际都是由一个Map来维护的,并且每个Thread自己持有一个Map,而对于每个Thread持有的Map数据的访问或操作都只能通过调用ThreadLocal的方法来达成。
整体的数据结构如下图:
暂且不管那个ThreadLocalMap的结构,从图中直观的可以看到每一个Thread持有一个私有Map的结构,和前面想的方案的数据结构对比一下,其实就是把Thread的Key去掉了,直接从Thread里就可以找到自己存放数据的Map了。
前面已经介绍过,所有线程变量都是通过ThreadLocal来操作的,我们来看下ThreadLocal的使用:
public class StaticThreadLocalTest {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
private static Integer num = null;
public static void main(String[] args) {
MyTask task = new MyTask();
new Thread( task ).start();
new Thread( task ).start();
}
static class MyTask implements Runnable {
@Override
public void run() {
// 每个线程随机生产一个数
Integer count = new Random().nextInt(100);
System.out.println(Thread.currentThread().getName() + ", count: " + count );
// 模拟业务耗时
try{
Thread.sleep(1000);
}catch (Exception e) {
}
// 存储数据
num = count;
threadLocal.set(count);
// 获取数据
System.out.println( Thread.currentThread().getName() + ", num: " + num + ", threadLocal: " +threadLocal.get() );
// 模拟业务耗时
try{
Thread.sleep(100);
}catch (Exception e) {
}
// 移除当前线程所存的数据
threadLocal.remove();
}
}
}
以上例子是在网上代码进行了一些改造,这里对比num
和threadLocal
的输出值,来感受ThreadLocal的实际作用。
输出内容:
Thread-1, count: 82
Thread-0, count: 31
Thread-1, num: 82, threadLocal: 82
Thread-0, num: 82, threadLocal: 31
多次执行发现num值会是和Thread对应的count不一样而threadLocal值是和count一致。
debug代码输出:
Thread-1, count: 35
Thread-0, count: 59
Thread-1, num: 35, threadLocal: 35
Thread-0, num: 59, threadLocal: 59
也可以达到num和ThreadLocal都和对应Thread的count一致。
由此可以感知到,ThreadLocal是保证了变量数据是线程隔离的。而在实际应用中,当使用的每个线程都需要用到一个对象,那么就可以把这个对象的副本放到ThradLocal中,通过这个ThreadLocal操作对象的时候,其实是对Thread独有的副本进行操作,互不干扰。
核心结构分析
从前面的使用代码可以看出,定义一个ThradLocal可以提供给多个线程使用,线程通过ThradLocal的方法来存取属于自己的数据集,而每个线程也是可以使用多个ThradLocal的。回顾数据结构图,那个属于Thread的Map是核心所在,接下去将花费大量篇幅对这个Map进行全面细致的分析,其中会涉及到大量关联知识点,都会尽可能的展开深究,以求展示完整脉络。
熟悉HashMap结构的朋友应该知道,它是以数组+链表+红黑树实现的,后续的分析中可以参照HashMap进行比对学习。
ThreadLocalMap的最小单元是Entry,一个Entry存着Key和Value,Key始终是ThreadLocal,Value是使用者存的对象,其中这个Key是弱引用的。
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
注释中特别对这个弱引用做了特别的解释,这是ThradLocal中非常关键的设计
- WeakReference是什么
当一个对象仅仅被weak reference(弱引用)指向, 而没有任何其他strong reference(强引用)指向的时候, 如果这时GC运行, 那么这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。
-
这个Key是ThreadLocal,所以这个弱引用需要产生作用,取决于ThreadLocal是否被强引用着。
-
Map的设计机制中是对这个key是为null的情况做判断的,如果是null,就认为是过期无用的数据,可以丢弃,注意在注释上的
expunged
这个单词,后续方法中清理的时候就用这个单词的方法。
Map中维护着一个Entry
数组:
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
对于数组,随着存储数据逐渐增多,必然需要扩容,这点在HashMap
中也是有的,另外特别要求是数组的长度必须保证是二次幂,这个和Hash算法有关,后续会关联到。
源码分析
了解好基本的一些信息后,我们深入ThreadLocalMap
的方法源码。
ThreadLocalMap
set方法:
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be 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;
// hash算法 算出数组下标
int i = key.threadLocalHashCode & (len-1);
// 开始从算出的下标开始向后判断每个槽位有没有不为空的
// 如果不为空,则有三种情况:
// 1,已存储的Entry的key和set的key相同;
// 2,存储的Entry的key是null,表示为过期Entry;
// 3,hash算法计算后和另一个key的hash计算后是同一个数组下标,表示发生hash冲突
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 已经有相同key对应的Entry存在,也就找到key存的位置了,直接替换value即可
if (k == key) {
e.value = value;
return;
}
// 另一种情况是存在的Entry内的key是null,表示已被回收,
// 这个就是key弱引用的关键机制,在set的时候也有判断,判断出来后就调用replaceStaleEntry方法
if (k == null) {
// 用新值替换过期槽上的值,不过还做了其他的事,这个方法后面分析
replaceStaleEntry(key, value, i);
return;
}
// 注意hash冲突的情况就继续循环数组,往后面继续找可以set的槽位
}
// 到这里说明i下标下的槽位是空的,直接new一个Entry放入
tab[i] = new Entry(key, value);
// 更新已经使用的数组数量
int sz = ++size;
// 做一次清理过期槽位(注意这个some),如果没有清理出一个并且数组使用量已超过扩容阀值,则进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 重新调整数组
rehash();
}
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
// 先进行一次全扫描的清理过期槽位
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
// 为防止迟滞现象,使用四分之三的阀值和size进行比较来确定是否进行扩容
// 这里说的迟滞现象意思是这样的,全数组的扫描清理过期数据只在rehash方法时触发,
// 所以有可能出现的一种情况是有一些需要被清理的数据一直没有被清理,
// 而在当判断出size超过扩容阀值后进入rehash方法进行一次全数组扫描清理,
// 这些没有被清理的数据在此时会被清理,如果只是少量则可以看作是运行过程中的迟滞效果,
// 所以在判断是否真正进行扩容的时候,用减小四分之一的阀值去比较。
if (size >= threshold - threshold / 4)
resize();
}
注:迟滞现象
以下四个场景的图基本描述清楚了set
方法的流程。
set
方法场景一:
set
方法场景二:
set
方法场景三:
set
方法场景四:
解决hash冲突的方法
- 开放地址法 核心思想是当出现冲突的时候,以冲突地址为基础,计算出下一个table上的地址,再冲突则重复计算,知道找到空位,ThreadHashMap的计算方式就是往后移一位,当然还有其他的计算方式。
- 拉链法 这个也是java选手最熟悉的,因为大家都了解过HashMap的hash冲突时的解决方式就是建立链表形式解决,这个就是拉链法的核心思想,在原先的table数据结构基础上再增加另一个数据结构存储hash冲突的值。
- 再散列法 准备多个hash方法,依次执行直到找到一个空位放入
前面set方法场景四就是hash冲突的场景,这里采用了开放地址法来解决hash冲突。
疑问:如果在场景四情况下,因为遍历是从hash计算后的下标开始往后的,会不会出现往后的槽位都不能放数据的情况呢?
解释:不会出现这种情况,这里容易忽视for循环中的nextIndex(i, len)
方法,代码:return ((i + 1 < len) ? i + 1 : 0);
,可以看到当下标到达len-1,就会将下标转成数组的头部,这就保证了循环数组的过程是头尾相连的,也就是说遍历不是从hash计算的下标到len-1就结束了,而是会遍历全部数组,那么再加上每次set
方法最后都会检测槽位占用情况来判断是否进行rehash()
,就保证了数组是不会因为满而放不下数据的情况,另外prevIndex(int i, int len)
方法也是保证遍历数组是头尾相连的,提前知道这点有助于阅读源码时的理解速度
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
HASH算法
作为Map,需要对Key进行Hash算法计算得到对应下标,而ThreadLocalMap的算法如下:
int i = key.threadLocalHashCode & (len-1);
前面重点提到过数组的长度是二的幂次,所以(len-1)表示二进制里所有位都是1,比如2的3次方减1的二进制表示:1111,这里进行与操作作用就是从threadLocalHashCode数值里取len对应的位数部分。
/**
* ThreadLocals rely on per-thread linear-probe hash maps attached
* to each thread (Thread.threadLocals and
* inheritableThreadLocals). The ThreadLocal objects act as keys,
* searched via threadLocalHashCode. This is a custom hash code
* (useful only within ThreadLocalMaps) that eliminates collisions
* in the common case where consecutively constructed ThreadLocals
* are used by the same threads, while remaining well-behaved in
* less common cases.
*/
private final int 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;
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode = new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
从上面的代码可以看到threadLocalHashCode
是nextHashCode变量加一个魔法数(HASH_INCREMENT),而nextHashCode是类静态公共变量,其AtomicInteger类型确保nextHashCode变量是线程安全的,在new出ThreadLocal的时候,会执行到nextHashCode()
计算threadLocalHashCode
,这样每个ThreadLocal拿到的threadLocalHashCode都是不同,并且是按一个固定差值规则产生的。
0x61c88647
这个神奇的数字一定有玄机吧。这就是传说中的斐波那契散列。
0x61c88647
转成十进制为:-2654435769
黄金分割数计算公式:(Math.sqrt(5) - 1)/2
以下计算可得-2654435769
double t = (Math.sqrt(5) - 1)/2;
BigDecimal.valueOf(Math.pow(2,32)* t).intValue()
每次哈希的计算就是每次累加黄金比例参数乘以2的32幂次,然后再和2的幂次取模,如此得到分布均匀的哈希值。
resize方法
扩容操作是只有在set
方法触发,对于数组为基础的数据结构大多会需要考虑扩容的能力。
/**
* Double the capacity of the table.
* 扩大两倍容量
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
// 新建数组
Entry[] newTab = new Entry[newLen];
int count = 0;
// 遍历老数组上全部元素,重新计算hash值然后分配到新数组上
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
// 过期数据
if (k == null) {
e.value = null; // Help the GC
} else {
// 重新根据新的数据长度计算hash值
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
// 设置扩容阀值 - 固定是数组长度的三分之二
setThreshold(newLen);
// 重置已经使用的数组数量
size = count;
// 重置数组
table = newTab;
}
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
remove方法
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 一样的hash算法
int i = key.threadLocalHashCode & (len-1);
// 因为采用开放地址法解决hash冲突,
// 所以找到下标后开始往后一个个比对,直到找打要删除的key对应的Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 比对key
if (e.get() == key) {
// Entry是WeakReference,清除弱引用
e.clear();
// 执行一次探测式清理
expungeStaleEntry(i);
return;
}
}
}
replaceStaleEntry方法
在set
方法中的场景三中,是调用replaceStaleEntry方法然后结束,从场景角度看,这个方法只需要帮忙做一个事,就是帮我们替换下新的值到已经过期的槽中就行了。然而事情不简单,我们通过下图了解怎么不简单。
我们假设数组中4下标是hash冲突的值,此时已经有四个元素hash冲突后依次放在4,5,6,7的槽位里,然后5号槽位过期了。
此时set进来一个value,计算出来下标是4,发现4号槽位不为null,并且key也不为null,就继续往后找,那么就会发现5号槽位是已过期数据,如果直接替换就会发生图中的情况,会有两个相同key的Entry存入数组中,明显这已经不符合Map的特性。
有的同学应该会马上想到可以依次比对4个槽位,然后找出7号槽位进行替换就可以了,的确,这个思路没错。再思考下两个点:
- 现在已经明显发现有一个过期数据在哪个槽位
- 如果需要遍历后续的槽位,那么也可以判断出哪些是过期数据
带着这个思路,接下来我们查看replaceStaleEntry
源码,看下它是怎么实现的。
/**
* Replace a stale entry encountered during a set operation
* with an entry for the specified key. The value passed in
* the value parameter is stored in the entry, whether or not
* an entry already exists for the specified key.
*
* As a side effect, this method expunges all stale entries in the
* "run" containing the stale entry. (A run is a sequence of entries
* between two null slots.)
*
* @param key the key
* @param value the value to be associated with key
* @param staleSlot index of the first stale entry encountered while
* searching for key.
*/
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;
// 从staleSlot位置开始往前面循环,直到最近的空槽出现才停止,
// 注意这个prevIndex也是环形头尾相连的
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
// 在循环过程中如果出现过期数据,则把下标赋值给slotToExpunge
// 注意,如果没有出现过期数据,slotToExpunge=staleSlot
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
// 从staleSlot位置开始往后面循环,直到最近的空槽出现才停止
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.
// 这里注释解释得比较清楚,就是如果我们找到了相同key的槽,那么就只需要替换旧的value就行了
// 另外,就是清理过期数据
if (k == key) {
// 赋新值
e.value = value;
// 交换逻辑,就是把staleSlot槽位entry和i槽位进行交换
// 交换后那个过期数据就到了i下标的槽位上
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
// 如果slotToExpunge依旧等于staleSlot表示在向前循环查找过期数据时没有出现过期数据
// 并且在向后循环遍历中也没有出现过期数据,因为一旦出现过就会在下面的代码中把i赋值给slotToExpunge
// 所以此时需要清理的起始位置就是i
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.
// k等于null,代表着出现过期数据了,
// 那么如果此时slotToExpunge还是等于staleSlot说明在向前的循环中没有出现过期数据
// 清理过期数据就可以从i开始,因为即使在接下去的循环中出现过期数据,这个判断也不成立了因为slotToExpunge已经不等于staleSlot了
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
// 向后遍历后没有发现重复key的槽位,那么就可以在这个槽位上替换这个过期数据了
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
// 这里意思是如果slotToExpunge等于staleSlot,
// 就表示除了staleSlot位置是过期数据,向前遍历和向后遍历都没有找到一个过期数据
// staleSlot位置已经替换好值了,就不需要进行清理数据
// 但是只要两者是不相同的说明还有其他的过期数据,就需要进行清理操作
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
看完replaceStaleEntry
源码,印证了前面的解决思路,代码中不仅只是在往后遍历时查看了数据是否过期,还向前遍历查看了一些。
如何找到清理开始位置
对于如何找到清理开始位置,这个方法中写了很多代码,目的是以staleSlot为起点向前找到第一个空槽位,然后以staleSlot为起点向后找到最近的空槽位,如果在设置好新值后这两个空槽位之间还存在过期数据,就触发清理操作。
先遍历前面数据:
-
前面有过期数据:从staleSlot向前遍历时发现了过期数据,slotToExpunge则会设置成过期数据的下标,有多个过期数据的情况则slotToExpunge会设置成最远的下标
-
前面没有过期数据:slotToExpunge等于staleSlot
然后遍历后面数据:
- 当后面有过期数据情况,如果前面没有过期数据,则把slotToExpunge设置成staleSlot后面的过期数据槽位下标,精确了开始位置
- 当发现有key值相同情况,此时是把staleSlot上过期的数据和相同key槽位进行交换,所以此时已经明确相同key槽位是过期数据,那么从这里触发往后面的清理,然后中断往后的遍历结束方法
最后兜底:
- 如果往后的循环没有中断就意味着没有找到相同key的槽位,所以staleSlot下标的槽位就是要放数据的地方,新数据放入后,这个过期的槽位就不存在了,从前面两个遍历来看,只要发现新的过期数据slotToExpunge必然变化,并且保证slotToExpunge是最前面的过期槽位,所以只需要判断slotToExpunge是否还等于staleSlot,如果不等于说明遍历环节时除了staleSlot位置,还有其他槽位上有过期数据,那么就触发从slotToExpunge位置往后进行清理
清理操作
代码如下:
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
清理工作就是进行expungeStaleEntry方法从slotToExpunge位置往后到最近的空槽位探测式清理,然后使用cleanSomeSlots
方法进行启发式清理。
解释一下这两种清理方式
-
探测式清理(expungeStaleEntry)
这个就是正常遍历下去一个个查验是否过期,然后清除
-
启发式清理(cleanSomeSlots)
这个是在需要遍历的数据比较多的时候,要将全部的过期数据全部清理是比较耗时的,在正常的场景下,不太可能定时进行清理,我们也发现实际清理的触发是在
set
方法或remove
方法,所以就进行挑选几个形式的方式进行查验和清除操作
expungeStaleEntry方法
先看探测式清理下expungeStaleEntry
方法,在replaceStaleEntry
和remove
方法中都有用到,在staleSlot下标到下一个空槽中间,擦除全部过期数据
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
// staleSlot传入参数就是已经知道这个位置是过期数据,不仅把这个位置清除一下
// 还会向后继续遍历到最近的空槽,遍历过程中有过期数据也会清理
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// staleSlot下标位置擦除操作
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
// 从staleSlot下标开始往后查
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// key是null的情况,key已被回收,是过期数据
if (k == null) {
// 执行这个方法的任务:擦除操作
e.value = null;
tab[i] = null;
// 同步数组已用量
size--;
} else {// key不为空的情况
// 进行一次hash计算,得出这个数据应该在的下标位置h
int h = k.threadLocalHashCode & (len - 1);
// 应该在的下标位置和当前下标进行比较 如果不相等,就表示这个数据是在放入的时候出现hash冲突,往后移动了位置的
// 那么在擦除的操作里可能它的前面已经有过期数据被擦除了,就需要把它往前移上去
if (h != i) {
// 开始移动
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
// 确定移到的位置:从h下标开始往后找到最近的空槽
while (tab[h] != null)
h = nextIndex(h, len);
// 移动位置
tab[h] = e;
}
}
}
return i;
}
看来这个方法需要做的是就是遍历数据然后检查是否过期,如果是就清理过期数据,但是又是因为开放地址法,所有hash冲突的数据都是确保挨着的,当你在清理数据的时候是不是需要考虑到一个场景:
假设4,5,6槽位的数据在放入的时候,key的hash值是相同的,都是4,所以按照顺序挨着放在一起,那么此时5号槽位上的key过期了,那么触发expungeStaleEntry
方法的时候会清理这个槽位,如果不做其他操作,4,6槽位的数据中间就有一个放着null的槽位。在get
方法时如果要找到6槽位的的key必然是先得到hash值是4,然后比对key1,发现不相同,就往后移动继续比对key,但是因为中间的空槽,6槽位就无法到达。所以就需要再做一件事就是把后面的6槽位往前移动,往前移动的依据就是重新计算hash值,然后再按照set
方法一样再放一遍。
cleanSomeSlots方法
和前面探测式清理对应的启发式清理是在方法注释中就解释了: Heuristically scan
。前面的已经解释过这个机制的作用。
依据源码,我们详细了解一下这个机制的实现细节。因为这个方法比较特殊,作者在注释中解释得比较清楚,所以先机器翻译下这个注释来理解下会比较好。
启发式扫描一些单元格以查找过时的条目。 这在添加新元素或删除另一个陈旧元素时调用。 它执行对数扫描,作为不扫描(快速但保留垃圾)和扫描次数与元素数量成正比之间的平衡,这将找到所有垃圾但会导致某些插入花费 O(n) 时间
参数
n
控制扫描元素个数,在没有过期数据被扫描到的情况下,会扫描log2(n)
个元素,反之,则需要增加扫描log2(table.length)-1
个元素。这个方法在set
调用时,n
为元素数size
,当replaceStaleEntry
调用时传的是数组长度。
方法中的while
循环条件(n >>>= 1) != 0
的意思是n向右移一位如果等于0则退出循环,这个其实就是n的2的幂次数累减然后判断是否不等于0,换算成数学公式就是:log2(n)-1 != 0
。
/**
* Heuristically scan some cells looking for stale entries.
* This is invoked when either a new element is added, or
* another stale one has been expunged. It performs a
* logarithmic number of scans, as a balance between no
* scanning (fast but retains garbage) and a number of scans
* proportional to number of elements, that would find all
* garbage but would cause some insertions to take O(n) time.
*
* @param i a position known NOT to hold a stale entry. The
* scan starts at the element after i.
*
* @param n scan control: {@code log2(n)} cells are scanned,
* unless a stale entry is found, in which case
* {@code log2(table.length)-1} additional cells are scanned.
* When called from insertions, this parameter is the number
* of elements, but when from replaceStaleEntry, it is the
* table length. (Note: all this could be changed to be either
* more or less aggressive by weighting n instead of just
* using straight log n. But this version is simple, fast, and
* seems to work well.)
*
* @return true if any stale entries have been removed.
*/
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就设置成数组长度,相当于如果有出现过期数据,就增加扫描次数,这里是增大n来实现
n = len;
removed = true;
// 出现过期数据就用expungeStaleEntry方法清理,所以这里是会从i下标到下个空槽内都会扫描一遍
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
这种没有进行全量扫描的清理过期数据的方式在很多类似场景中都有相似的实现,比如Redis
的Key
主动删除方式:
Specifically this is what Redis does 10 times per second:
- Test 20 random keys from the set of keys with an associated expire.
- Delete all the keys found expired.
- If more than 25% of keys were expired, start again from step 1.
注意如果在检查的样本中有百分之二十五是过期的,就再继续进行清理,和上面的扩大n是一个思想。
getEntry方法
这个操作是Map结构提供出来get操作,比较简单。也注意到hash值相同的Entry都是挨着的,如此才可以保证get
到正确的value
。
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
// 熟悉的计算hash值的方式
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 找到key情况 直接返回值
if (e != null && e.get() == key)
return e;
else // 没找到就需要用到开放地址法找key的流程了
return getEntryAfterMiss(key, i, e);
}
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
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;
}
构造方法
ThreadLocalMap
的构造方法有两个,第二个方法是专门用于可集成的ThreadLocal,只被ThreadLocal的createInheritedMap
方法调用,特殊的就是把传入的ThreadLocalMap
遍历复制到新的ThreadLocalMap
。
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
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);
}
/**
* Construct a new map including all Inheritable ThreadLocals
* from given parent map. Called only by createInheritedMap.
*
* @param parentMap the map associated with parent thread.
*/
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// childValue可扩展
// 默认InheritableThreadLocal是返回传入的值,
// 所以特别注意传入的parentMap的value和新创建的ThreadLocalMap的value是一个对象
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
ThreadLocal
构造方法
/**
* Creates a thread local variable.
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}
1.8版本引入了函数式接口方式:withInitial
方法
/**
* Creates a thread local variable. The initial value of the variable is
* determined by invoking the {@code get} method on the {@code Supplier}.
*
* @param <S> the type of the thread local's value
* @param supplier the supplier to be used to determine the initial value
* @return a new thread local variable
* @throws NullPointerException if the specified supplier is null
* @since 1.8
*/
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
/**
* An extension of ThreadLocal that obtains its initial value from
* the specified {@code Supplier}.
*/
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
在1.8版本之前很多jdk的代码都是如BigDecimal
的代码那样使用匿名类的方式这么写的:
private static final ThreadLocal<StringBuilderHelper>
threadLocalStringBuilderHelper = new ThreadLocal<StringBuilderHelper>() {
@Override
protected StringBuilderHelper initialValue() {
return new StringBuilderHelper();
}
};
通过覆盖initialValue()
方法,就可以在线程执行get()
方法时获得初始值。
而1.8后,推荐的写法就变成这样:
private static final ThreadLocal<StringBuilderHelper>
threadLocalStringBuilderHelper = ThreadLocal.withInitial(() -> new StringBuilderHelper());
通过Supplier
简化代码的方式值得借鉴。
get方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
// 拿到线程实例内的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// this就是key
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 找不到情况执行
return setInitialValue();
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
// 默认返回的是Thread的threadLocals
// 这个方法在子类InheritableThreadLocal中覆盖后返回的是Thread的threadLocals
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
set方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 调用ThreadLocalMap的set方法
map.set(this, value);
else
// 线程实例还未初始化ThreadLocalMap的情况下,需要进行一次初始化
createMap(t, value);
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
createInheritedMap方法
/**
* Factory method to create map of inherited thread locals.
* Designed to be called only from Thread constructor.
*
* @param parentMap the map associated with parent thread
* @return a map containing the parent's inheritable bindings
*/
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
这个方法只有被Thread的构造方法所调用,看下调用代码片段:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}
其实就是把父线程的inheritableThreadLocals
通过createInheritedMap
方传递给子线程的inheritableThreadLocals。
我们在文章的开始就清楚Thread持有一个ThreadLocalMap来存储本地变量,其实Thread还持有一个ThreadLocalMap是:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
注释中解释,inheritableThreadLocals是InheritableThreadLocal
来维护的,我们看下是怎么实现的。
InheritableThreadLocal
是继承ThreadLocal的一个类,它覆写了三个方法,后面两个方法直接把原先默认的Thread的threadLocals
替换成了Thread的inheritableThreadLocals
。这样所有ThreadLocal的方法关联的ThreadLocalMap都会指向到Thread的inheritableThreadLocals
。因为线程创建时默认是会把父线程的inheritableThreadLocals
传递给子线程,所以只要使用InheritableThreadLocal,就相当于父线程也是使用inheritableThreadLocals
,子线程使用基于父线程的inheritableThreadLocals
。
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* Computes the child's initial value for this inheritable thread-local
* variable as a function of the parent's value at the time the child
* thread is created. This method is called from within the parent
* thread before the child is started.
* <p>
* This method merely returns its input argument, and should be overridden
* if a different behavior is desired.
*
* @param parentValue the parent thread's value
* @return the child thread's initial value
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
而childValue
方法是为了在使用InheritableThreadLocal
时的扩展,比如在createInheritedMap
方法创建ThreadLocalMap的时候,把父线程中的ThreadLocalMap的内容复制到子ThreadLocalMap中的时候会调用childValue
方法,可以进行扩展这个方法。例子代码:
private InheritableThreadLocal<Map<String, String>> inheritableThreadLocal = new InheritableThreadLocal<Map<String, String>>() {
protected Map<String, String> childValue(Map<String, String> parentValue) {
return parentValue == null ? null : new HashMap(parentValue);
}
};
特别注意,在前面写到的InheritableThreadLocal的childValue
方法是返回传入的值,所以在ThreadLocalMap拿父线程的Map来构造的时候放入自己Map的Value其实是和父线程中Map所持有的是同一个对象,那么在子线程使用的过程中修改了这个value,那么父线程也会受到影响。
探索
关于static修饰
再很多规范甚至作者都推荐ThreadLocal
使用static
修饰,因为这样做的话可以避免重复创建ThreadLocal
,节省空间使用,并且是延长了ThreadLocal的生命周期,在平时研发代码时,大多是线程共享的,比如一次请求的线程使用ThreadLocal,那么ThreadLocal是请求级别,一般都需要调用remove
方法来清理线程内的本ThreadLocal对应的变量。
关键点是,前面已经介绍了ThreadLocalMap的Key是ThreadLocal的弱引用,如果适应static,那么这个弱引用就相当于失效了,因为static修饰的ThreadLocal始终有一个Class对象持有它,就不能触发弱引用机制进行回收。其实这个也很矛盾,在源码中做了一整套过期数据的清理机制,使用的时候一个static就没什么用了。
局限
ThreadLocal不能解决多线程之间的本地变量传递,所以扩展出了InheritableThreadLocal,他能支持父线程给子线程来传递数据,但是任然没有很好的解决在其他多线程之间的数据传递问题。
参考
https://juejin.cn/post/6947653879647617061#heading-8
https://segmentfault.com/a/1190000022663697
https://www.zhihu.com/question/35250439
https://jishuin.proginn.com/p/763bfbd652ed
https://juejin.cn/post/6844903938383151118
https://zhuanlan.zhihu.com/p/29520044
https://programmer.ink/think/deep-understanding-of-threadlocal-source-memory.html