在Thread
类的源码中,可以看到以下声明:
1 | public |
可以看出,Thread
的实例持有了对 ThreadLocal.ThreadLocalMap
实例 threadLocals
的引用。
ThreadLocal#get
1 | /** |
在 ThreadLocal
类的 get()
方法中,首先获取到当前线程 t
,然后使用 getMap(Thread t)
方法获取到当前线程 t
中持有的 ThreadLocal.ThreadLocalMap
实例 threadLocals
,然后再尝试从该 threadLocals
中获取当前 ThreadLocal
实例对应的值,如果 threadLocals
为空或者未获取到 ThreadLocal
实例对应的值,则执行以下的初始化逻辑:
1 | /** |
其中 initialValue()
是留给用户覆写的方法。注意,ThreadLocalMap
中的 key
为 ThreadLocal
实例。
ThreadLocal#set
1 | /** |
ThreadLocal
类的 set(T value)
方法实现与 get()
方法类似,此处不再赘述。
ThreadLocal#remove
1 | /** |
ThreadLocal
类的 remove()
方法实现也很简单,核心就是获取出当前线程持有的 threadLocals
实例,然后在该实例上操作即可。通过以上三个操作 ThreadLocal
实例的方法,我们知道,其操作都依赖当前线程持有的 ThreadLocal.ThreadLocalMap
实例 threadLocals
,且在 threadLocals
中,key 为 ThreadLocal
实例,即对应源码中的 this
,value 为用户使用 ThreadLocal
来实际存储的对象的引用。
ThreadLocal.ThreadLocalMap
我们再看看 ThreadLocal
中用到的核心数据结构 ThreadLocalMap
,该类作为 ThreadLocal
的静态内部类定义,如果没有看过 HashMap
实现原理的建议先看看 HashMap。回到 ThreadLocalMap
,先看看 Entry
的定义:
1 | /** |
可以看出,ThreadLocalMap
由 Entry
数组组成,初始容量为 16,且注释明确要求容量必须为 2 的 n 次幂,原因为当容量为 2 的 n 次幂时,定位元素所在数组下标的计算可以由 取余操作 优化为 与运算 实现,效率更高,关于 2 的 n 次幂带来的弊端可以参考 HashMap,此处不再赘述。仔细看 Entry
类的定义,其继承了 WeakReference
且将 Entry
的 key
作为弱引用,value
依然是强引用。再看看 ThreadLocalMap
的构造函数:
1 | /** |
可以看出定位 key
所在数组下标采用了 与运算 实现,最后计算并设置了触发扩容的阈值,注意此处使用的负载因子为 2/3
,原因可参考:Open Addressing。回到构造函数,其中不得不提的是 firstKey.threadLocalHashCode
,我们看一下 ThreadLocal
中关于 threadLocalHashCode
的相关代码:
1 | /** |
从注释中我们知道 ThreadLocalMap
采用线性探测处理 hash 冲突,每一个 ThreadLocal
实例的 threadLocalHashCode
都会使用线程安全的静态变量 nextHashCode
加上 0x61c88647
得到,那么这个累加的值是如何得到的呢?首先根据源码可以看出 threadLocalHashCode
字段定义的类型为 int
,在 Java 中,基本类型是有符号的,那么可以知道 threadLocalHashCode
可以使用的范围为 [Integer.MIN_VALUE, Integer.MAX_VALUE]
,这一段范围值的长度为 232 = 4294967296。我们将这段范围进行黄金分割,分割后较长的段长度为:(232) / ((1 + sqrt(5)) / 2) = 2654435769,较短的段长度为 4294967296 - 2654435769 = 1640531527。而 Java 中 Integer.MAX_VALUE
的十进制表示为 2147483647,较长段长度 2654435769 因为大于了 Integer.MAX_VALUE
无法用 int
直接表示,较短的段长度为 1640531527,可以用 int
表示,我们看看较长段长度 2654435769 的二进制表示:
1 | 2654435769: |
可以看出如果把 2654435769 用 int
表示,则转换为十进制为 -1640531527,你会发现,数值部分竟然与较短段长度一致,这也是黄金分割点的神奇之处。将数值部分 1640531527 转换为十六进制表示则为 0x61c88647
,与源码中的值一致,猜测这就是作者选择 0x61c88647
作为 HASH_INCREMENT
值的原因。即每个 ThreadLocal
实例的 threadLocalHashCode
相差的值为整个 int
范围段进行黄金分割后较短部分的长度。那么为何要对整数域进行黄金分割并选用上述计算的值作为 HASH_INCREMENT
呢?实际上这是一种特殊的乘法散列,只不过常数使用的 HASH_INCREMENT
,也被称作斐波拉契哈希,其能保证在整个整数域上均匀分布,可参考:Fibonacci Hashing。当然,也有另一种说法是说采用的是黄金分割点的另一种变体,参见:What is the meaning of 0x61C88647 constant in ThreadLocal.java - Stack Overflow。
在 Kryo 的 ObjectIntMap 类中也可以看到类似的实现,只不过是根据 Long
进行的黄金分割:
1 | /** Returns an index >= 0 and <= {@link #mask} for the specified {@code item}. |
我们再看看 ThreadLocalMap
的 set
方法实现,源码位于 ThreadLocal.java at jdk8-b120:
1 | /** |
可以看出,其在定位 tab
数组的索引时没有像 HashMap
一样引入防御式的哈希函数,因为当前实现的哈希值由以上我们分析的逻辑计算得出,不像 HashMap
受用户编写的 hashCode()
函数的质量影响。所以可以认为当前哈希值计算函数质量良好,此处直接使用了 key.threadLocalHashCode
进行槽位索引计算。对于冲突,采用的是步长为 1 的基于线性探测的解决方案,相比 HashMap
的单独链表法,具有更好的缓存性能。
注意其中的 k == null
判断,此时是在判断 Entry
实例的 key
即弱引用指向的对象是否已经为空,如果为空,说明已经被 GC,此时将调用 replaceStaleEntry
方法将 value
设置至索引 i
对应的槽位:
1 | /** |
根据代码中的注释我们知道,该方法除了设置值至对应的 Slot 外,还会清除废弃的 Entry 实例。接着我们看看在上面方法中调用的 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
中的两个方法,先看 expungeStaleEntry(int staleSlot)
方法:
1 | /** |
可见 expungeStaleEntry(int staleSlot)
方法的实现还是比较简单的,再看看 cleanSomeSlots(int i, int n)
方法的实现:
1 | /** |
该方法比较简单,且注释非常清楚,启发式扫描,在不扫描与元素数量成比例扫描之前取得平衡。此时我们回到 set
方法:
1 | /** |
可知,如果哈希表中之前不存在相同的 key
且没有废弃的 Entry
实例可以用于存放当前需要设置的 key
,那么此时需要创建新的 Entry
实例并设置至 tab[i]
,设置并更新 size
之后将会调用 cleanSomeSlots
方法清理部分槽位,如果连一个槽位也没有得到清理,那么此时将比较 sz
是否大于等于 threshold
,即如果当前 size
已经达到阈值,则调用 rehash()
尝试进行重新哈希:
1 | /** |
以上几个方法的实现都比较简单,注释直接写在源码中了,此处不再赘述。
关于该类的使用场景,可以看几个例子:
在 DataSourceTransactionManager.java at v5.3.15 中,会将获取到的 JDBC 连接进行包装并绑定至当前线程,核心代码如下:
1
2
3
4// Bind the connection holder to the thread.
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}TransactionSynchronizationManager
的部分源码位于 TransactionSynchronizationManager.java at v5.3.15:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
/**
* Bind the given resource for the given key to the current thread.
* @param key the key to bind the value to (usually the resource factory)
* @param value the value to bind (usually the active resource object)
* @throws IllegalStateException if there is already a value bound to the thread
* @see ResourceTransactionManager#getResourceFactory()
*/
public static void bindResource(Object key, Object value) throws IllegalStateException {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
Assert.notNull(value, "Value must not be null");
Map<Object, Object> map = resources.get();
// set ThreadLocal Map if none found
if (map == null) {
map = new HashMap<>();
resources.set(map);
}
Object oldValue = map.put(actualKey, value);
// Transparently suppress a ResourceHolder that was marked as void...
if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) {
oldValue = null;
}
if (oldValue != null) {
throw new IllegalStateException(
"Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread");
}
}在 StringCoding 中,使用
ThreadLocal
保证了字符串编码器与解码器的线程安全。在 HikariCP 连接池的核心类 ConcurrentBag.java 中也有对ThreadLocal
的使用。在 Dubbo 的 ThreadLocalKryoFactory 类中,使用
ThreadLocal
为每个线程创建Kryo
实例以规避Kryo
实例被多线程操作的线程安全问题,因为Kryo
实例不是线程安全的,参见:Thread safety.
Reference
ThreadLocal (Java Platform SE 8 )
Golden ratio - Wikipedia
Why should Java ThreadLocal variables be static - Stack Overflow
Fibonacci Hashing: The Optimization that the World Forgot (or: a Better Alternative to Integer Modulo) | Probably Dance
Why does ThreadLocalMap.Entry extend WeakReference? - Stack Overflow