在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.
References
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