ThreadLocal 源码
此类提供线程局部变量。这些变量不同于它们的普通对应物,因为每个访问一个线程局部变量的线程(通过其 get 或 set 方法)都有自己独立初始化的变量副本。ThreadLocal
实例通常是类中的私有静态字段,这些类希望将状态与线程关联(例如,用户 ID 或事务 ID)。
例如,下面的类生成每个线程本地的唯一标识符。线程的id在第一次调用ThreadId. get() 时被分配,并且在后续调用中保持不变。
import java. util. concurrent. atomic. AtomicInteger;
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId. getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId. get();
}
}
每个线程都持有对其线程局部变量副本的隐式引用,只要线程处于活动状态并且ThreadLocal
实例是可访问的; 线程消失后,线程本地实例的所有副本都受到垃圾回收 (除非存在对这些副本的其他引用)。
public class ThreadLocal<T> {
/**
* ThreadLocals依赖于附加到每个线程的每线程线性探针哈希映射 (thread. threadLocals和inheritableThreadLocals)。
* ThreadLocal对象充当键,通过threadLocalHashCode搜索。
* 这是一个自定义哈希代码 (仅在ThreadLocalMaps中有用),它消除了在相同线程使用连续构造的ThreadLocals的常见情况下的冲突,同时在不太常见的情况下保持良好的行为。
*/
private final int threadLocalHashCode = nextHashCode();
/**
* 下一个要给出的哈希码。原子更新。从零开始。
*/
private static AtomicInteger nextHashCode = new AtomicInteger();
/**
* 连续生成的哈希码之间的差异
* 它将隐式的顺序线程本地 ID 转换为具有良好分布特性的乘法哈希值
*
* 为何使用 0x61c88647:
* 这个十六进制常量是黄金分割比例((sqrt(5)-1)/2 ≈ 0.618)的一个近似值。
* 在哈希算法中,它能够提供良好的随机分布特性,有助于减少哈希冲突。
* 特别适用于大小为 2 的幂的哈希表,因为可以通过位运算快速取模。
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 返回下一个哈希代码。
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
/**
* 返回当前线程对该线程本地变量的“初始值”。
* 当线程第一次通过 {@link #get} 方法访问该变量时,此方法会被调用,
* 除非线程此前已经调用了 {@link #set} 方法,在这种情况下,不会为此线程调用 {@code initialValue} 方法。
* 通常,每个线程最多调用一次该方法,但在后续调用 {@link #remove} 后再调用 {@link #get} 的情况下,可能会再次调用。
*
* <p>该实现默认返回 {@code null};如果希望线程本地变量具有非 {@code null} 的初始值,
* 则必须继承 {@code ThreadLocal} 类并重写此方法。通常会使用匿名内部类来实现。
*
* @return 该线程本地变量的初始值
*/
protected T initialValue() {
return null;
}
/**
* 创建一个线程本地变量。该变量的初始值由调用指定 {@code Supplier} 的 {@code get} 方法来确定。
*
* @param <S> 线程本地变量的值类型
* @param supplier 用于确定初始值的供给函数(supplier),不能为空
* @return 一个新的线程本地变量实例
* @throws NullPointerException 如果指定的 supplier 为 null
* @since 1.8
*/
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
/**
* 创建一个线程本地变量。
*
* @see #withInitial(java.util.function.Supplier)
*/
public ThreadLocal() {
}
/**
* 返回当前线程中该线程本地变量的副本的值。
* 如果该变量在当前线程中尚无值,则会先使用 {@link #initialValue} 方法的返回值进行初始化。
*
* @return 当前线程中该线程本地变量的值
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
/**
* 用于设置初始值的一个 set() 方法变体。
* 在用户重写了 set() 方法的情况下,使用此方法替代原生 set()。
*
* 用于在首次获取(get())一个 ThreadLocal 变量时,建立该变量的初始值。它会调用 initialValue() 方法来获取初始值,并将这个值存入线程本地的 ThreadLocalMap 中。
*
* 为什么不用 set()?
* 因为 set() 是一个公开方法,用户可能对其进行了重写(自定义了行为)。为了防止初始化过程受到用户自定义 set() 的影响,Java 使用了这个专门的 setInitialValue() 方法来进行初始值的设置。
*
* @return 初始值
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
/**
* 将当前线程中该线程本地变量的副本设置为指定的值。
* 大多数子类不需要重写此方法,而是完全依赖于 {@link #initialValue} 方法来设定线程本地变量的值。
*
* @param value 要存储在当前线程的该线程本地变量副本中的值
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
/**
* 移除当前线程中该线程本地变量的值。
* 如果该线程本地变量之后被当前线程通过 {@linkplain #get} 方法再次读取,
* 它的值将通过调用其 {@link #initialValue} 方法重新初始化,
* 除非在这期间当前线程已经通过 {@linkplain #set} 方法设置了新的值。
* 这可能导致在同一个线程中多次调用 {@code initialValue} 方法。
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* 获取与当前 ThreadLocal 关联的线程本地变量映射表(ThreadLocalMap)。
* 该方法在 InheritableThreadLocal 中被重写。
*
* @param t 当前线程
* @return 与当前线程关联的 ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 创建与 ThreadLocal 关联的映射表(ThreadLocalMap)。
* 该方法在 InheritableThreadLocal 中被重写。
*
* @param t 当前线程
* @param firstValue 映射表中初始条目的值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* 工厂方法,用于创建继承自父线程的线程本地变量映射表。
* 该方法设计为仅由 Thread 构造函数调用。
*
* @param parentMap 父线程关联的 ThreadLocalMap
* @return 一个包含父线程可继承绑定的新映射表
*/
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
/**
* 该方法在子类 InheritableThreadLocal 中被显式定义,
* 但在这里内部声明,目的是为了在不继承 map 类的情况下,
* 提供 createInheritedMap 工厂方法。
*
* 这种设计优于在方法中嵌入 instanceof 检查的方式。
*/
T childValue(T parentValue) {
throw new UnsupportedOperationException();
}
/**
* 一个继承自 ThreadLocal 的扩展类,其初始值来源于
* 指定的 {@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();
}
}
/**
* ThreadLocalMap 是一个专门定制的哈希映射表,仅用于维护线程本地变量(thread-local values)。
* 该类的所有操作都不会暴露到 ThreadLocal 类之外。
* 该类被定义为包私有(package private),以便允许在 Thread 类中声明其字段。
*
* 为了应对大量且长期存活的使用场景,哈希表中的键(key)采用弱引用(WeakReference)的形式。
* 然而,由于没有使用引用队列(reference queues),
* 所以无效的条目(stale entries)只有当哈希表空间不足时才会被真正清除。
*/
static class ThreadLocalMap {
/**
* 这个哈希映射中的每个条目(Entry)都继承自 WeakReference(弱引用),
* 使用其主引用字段作为键(这个键始终是一个 ThreadLocal 对象)。
*
* 注意:如果键为 null(即 entry.get() == null),
* 表示该键已经被垃圾回收,因此该条目可以被从表中清除。
* 在后续代码中,这类条目被称为 "stale entries"(过期条目)。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** 与当前 ThreadLocal 关联的值。 */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* 初始容量 —— 必须是 2 的幂(power of two)。
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 哈希表,根据需要进行扩容或缩容。
* table.length 的值必须始终是 2 的幂次方。
*/
private Entry[] table;
/**
* 哈希表中条目(键值对)的数量。
*/
private int size = 0;
/**
* 下一个需要扩容时的尺寸阈值。
*/
private int threshold; // Default to 0
/**
* 设置扩容阈值,以保证最坏情况下负载因子不超过 2/3。
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* 将 i 按 len 取模后递增。
*
* 更通俗地解释,该方法用于在哈希表索引中进行线性探测时,将当前索引 i 递增,并通过取模确保其不会超出数组长度 len 的范围。
* 如果递增后的值等于数组长度,则回到 0,实现一个环形数组的索引遍历。
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 将 i 按 len 取模后递减。
*
* 该方法用于在哈希表索引中进行反向线性探测时,将当前索引 i 递减,并通过取模确保其不会小于 0,从而实现对环形数组的索引安全访问。
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
/**
* 构造一个初始包含 (firstKey, firstValue) 的新映射表(ThreadLocalMap)。
* ThreadLocalMap 是惰性构造的,所以我们只在有至少一个条目需要插入时才会真正创建它。
*/
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);
}
/**
* 构造一个新映射表(ThreadLocalMap),包含给定父映射表中的所有可继承的 InheritableThreadLocal。
* 该方法仅由 createInheritedMap 调用。
*
* @param parentMap 与父线程关联的映射表(ThreadLocalMap)。
*/
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) {
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++;
}
}
}
}
/**
* 获取与指定键(key)关联的条目(Entry)。
* 该方法仅处理快速路径:直接命中已存在的键。
* 如果未命中,则会转交给 getEntryAfterMiss 方法处理。
* 这样设计是为了通过使该方法易于内联,从而提升直接命中的性能。
*
* @param key 线程本地对象(ThreadLocal)
* @return 与键关联的条目,如果不存在则返回 null
*/
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);
}
/**
* 当在直接哈希槽中未找到键时,使用的 getEntry 方法变体。
*
* @param key 线程本地对象(ThreadLocal)
* @param i 键的哈希码对应的表索引
* @param e 表中索引为 i 的条目(Entry)
* @return 与键关联的条目(Entry),如果不存在则返回 null
*/
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)关联的值(value)。
*
* @param key 线程本地对象(ThreadLocal)
* @param value 要设置的值
*/
private void set(ThreadLocal<?> key, Object value) {
// 我们不像 get() 方法那样使用快速路径,因为使用 set() 方法创建新条目的情况
// 至少和替换已有条目的情况一样常见。在这种情况下,快速路径往往会频繁失败。
// 这段注释说明了为何 set() 方法在实现上没有采用像 get() 一样的“快速路径”优化。
// 在 get() 中,大多数情况下是查找已存在的键,因此可以使用快速路径(直接命中)来提升性能。
// 但在 set() 中,插入新条目(create)和替换旧条目(replace)的频率差不多,所以如果强行用类似 get() 的快速路径,会导致很多情况下需要回退到慢速路径(如处理哈希冲突、清理过期条目等),反而影响整体效率。
// 因此,set() 方法选择统一走一个更通用的逻辑,避免单独设置“快速路径”。
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* 移除与指定键(key)关联的条目。
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
/**
* 在 set 操作中遇到过期条目时,用指定键的新条目替换它。
* 无论是否已存在该键对应的条目,传入的 value 都会被存储到条目中。
*
* 作为副作用,该方法会清除包含该过期条目的“运行段(run)”中的所有过期条目。
* (一个“运行段”是指两个 null 条目之间的连续条目序列。)
*
* 这个注释说明了 replaceStaleEntry 方法的作用是处理哈希冲突和过期条目的一种机制:
* 当在 set() 操作中发现某个条目的键为 null(即已被垃圾回收),就会调用此方法进行替换和清理。
* 它不仅会将新值插入到该过期条目的位置,还会顺带清理当前“运行段”中相邻的其他过期条目(stale entries)。
* “运行段(run)”指的是哈希表中两个 null 条目之间的连续非空条目区间。
*
* @param key 要设置值的键(ThreadLocal 对象)
* @param value 要与键关联的值
* @param staleSlot 在查找过程中遇到的第一个过期条目的索引位置
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 向前查找当前“运行段”中是否存在之前的过期条目。
// 我们一次性清理整个“运行段”,以避免因垃圾回收器成批释放引用而导致持续的
// 增量再哈希(rehashing)。(即,每次垃圾回收器运行时)
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 查找 key 或者运行段(run)中的尾随 null 槽位,以先出现者为准
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果我们找到了该键,则需要将其与过期条目交换位置,
// 以保持哈希表的顺序。
// 新产生的过期槽位,或者在其上方遇到的其他过期槽位,
// 可以被发送到 expungeStaleEntry 方法中,
// 用于移除或重新哈希“运行段”中的其他条目。
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果存在前面的过期条目,则从该条目开始清理(expunge)
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果在向前扫描(backward scan)过程中没有找到过期条目,
// 那么在查找键的过程中遇到的第一个过期条目,
// 就是当前“运行段”中仍然存在的第一个过期条目。
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果未找到键,则将新条目插入到过期槽位中
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* 通过重新哈希来清除一个过期条目(stale entry),该过期条目位于 staleSlot 和下一个 null 槽位之间。
* 在此过程中也会一并清除在 trailing null 之前遇到的其他过期条目。
* 参见 Knuth 第 6.4 节相关算法。
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清除位于 staleSlot 位置的过期条目
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 重新哈希直到遇到 null 条目为止
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// 与 Knuth 6.4 的 Algorithm R 不同,我们必须扫描到 null 槽位为止,
// 因为可能有多个条目是过期的(stale entries)。
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
/**
* 启发式地扫描一些槽位(cells),寻找并清理过期条目(stale entries)。
* 这个方法通常在以下两种情况下被调用:
* 1. 插入一个新的元素之后;
* 2. 清除一个过期条目之后。
*
* 它会执行对数级别的扫描次数(logarithmic number of scans),
* 目的是在性能和内存回收之间取得一种平衡:
*
* - 如果完全不进行扫描(no scanning):
* 插入操作会非常快,但会导致内存中保留大量“垃圾”(即 stale entries);
*
* - 如果扫描所有元素(full scanning):
* 能够找到并清除所有的“垃圾”,但这会让某些插入操作的时间复杂度变成 O(n);
*
* 所以我们采用折中策略,只扫描一部分槽位,既不会太慢,又能回收部分垃圾数据。
*/
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;
}
/**
* 重新整理(紧凑化)和/或扩容哈希表。
* 首先扫描整个哈希表,清除所有过期条目(stale entries)。
* 如果仅清除过期条目不足以显著缩小表的大小,则将哈希表容量加倍(扩容)。
*/
private void rehash() {
expungeStaleEntries();
// 使用较低的阈值来触发扩容,以避免迟滞现象(hysteresis)
if (size >= threshold - threshold / 4)
resize();
}
/**
* 将哈希表的容量翻倍(扩容)
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
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 {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
/**
* 清除表中的所有过期条目(stale entries)
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
}
}
InheritableThreadLocal 源码
/**
* 该类扩展了 <tt>ThreadLocal</tt> 以提供从父线程到子线程的值的继承:
* 当创建子线程时,子线程会接收所有继承的线程局部变量的初始值,
* 这些变量在父线程中有值。通常情况下,子线程的值会与父线程的值相同;
* 然而,通过在此类中重写 <tt>childValue</tt> 方法,可以将子线程的值设为父线程值的任意函数。
*
* <p>继承的线程局部变量用于维护必须自动传输到创建的任何子线程的每个线程属性(例如,用户ID、事务ID),
* 优先于使用普通的线程局部变量。
*
* @author Josh Bloch 和 Doug Lea
* @see ThreadLocal
* @since 1.2
*/
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* 计算继承的线程局部变量的子线程初始值,作为父线程值的函数。
* 在子线程创建时,从父线程调用该方法,子线程启动之前。
* <p>
* 该方法仅返回其输入参数,如果需要不同的行为,应重写该方法。
*
* @param parentValue 父线程的值
* @return 子线程的初始值
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* 获取与线程局部变量关联的映射。
*
* @param t 当前线程
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* 创建与线程局部变量关联的映射。
*
* @param t 当前线程
* @param firstValue 表的初始条目的值。
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
InheritableThreadLocal 使用场景
日志追踪、权限上下文传递、事务传播等场景下使用
InheritableThreadLocal
在父子线程之间传递上下文信息。也就是InheritableThreadLocal
适用于需要子线程继承父线程上下文状态的场景,避免显式传参。
InheritableThreadLocal<String> traceIdHolder = new InheritableThreadLocal<>();
// 父线程设置 traceId
traceIdHolder.set("abc123");
// 子线程可直接获取该值
new Thread(() -> {
System.out.println(traceIdHolder.get()); // 输出:abc123
}).start();