ThreadLocal是Java提供的一个线程局部变量工具类,在java.lang
包中,它允许每个线程拥有自己的变量副本,从而实现线程间的数据隔离。
内部类
在ThreadLocal类中,有2个关键的内部类,它们共同构成了ThreadLocal的核心功能。
SuppliedThreadLocal
这是一个继承自ThreadLocal的静态内部类,通过构造函数接收一个Supplier<? extends T>
类型的参数,这个Supplier用于在ThreadLocal变量第一次被访问时提供一个初始值。
查看代码
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();
}
}
- 属性:
supplier
: 类型为Supplier<? extends T>
,用于提供初始值。
- 方法:
initialValue()
: 重写了ThreadLocal的initialValue
方法,该方法会在ThreadLocal变量第一次使用时调用,并返回由supplier提供的值。
ThreadLocalMap
这是一个静态内部类,用于存储与线程相关的ThreadLocal变量值。它是一个定制化的哈希表,专门用于维护线程局部变量值,作为线程对象Thread
的一个属性,因此每个线程对象都有属于自己的ThreadLocalMap
,做到了天然的数据隔离。
查看代码
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; // Default to 0
// ...
}
属性
INITIAL_CAPACITY
: 表的初始容量,必须为2的幂。table
: 类型为Entry[]
,是实际的哈希表,用于存储键值对。size
: 表中条目的数量。threshold
: 下一个调整大小的阈值。
内部类
Entry: 这是一个继承自WeakReference<ThreadLocal<?>>
的静态内部类,用于表示ThreadLocalMap中的一个条目。它包含一个ThreadLocal对象的弱引用作为键,和一个与该ThreadLocal关联的值。
构造函数:接收一个ThreadLocal对象和一个值作为参数,创建一个弱引用指向ThreadLocal对象,并将值保存在value
属性中。
属性
在Java的ThreadLocal
类中有3个关键属性,它们帮助实现线程局部变量的功能:
threadLocalHashCode
- 类型:
int
- 作用:这是一个
final
属性,代表当前ThreadLocal
实例的唯一哈希码。这个哈希码用于在ThreadLocalMap
中作为键,以便为每个线程存储和检索其对应的值。该值在对象创建时通过调用nextHashCode()
方法进行初始化,并且之后不会再改变。
nextHashCode
- 类型:
AtomicInteger
- 作用:这是一个静态属性,用于生成下一个
ThreadLocal
实例的哈希码。
AtomicInteger
确保了在多线程环境中对nextHashCode
的修改是原子的,从而保证了每个ThreadLocal
实例都有唯一的哈希码。初始值为0,每次创建新的ThreadLocal
实例时,都会通过nextHashCode()
方法增加HASH_INCREMENT
。
HASH_INCREMENT
- 类型:
int
- 作用:这是一个静态常量,用于确定连续生成的哈希码之间的增量。这个特殊的值
0x61c88647
被选择是因为它能够提供良好的哈希分布,减少哈希冲突。
nextHashCode()
- 类型:
static int
- 作用:这是一个静态方法,用于获取并递增
nextHashCode
的值。每次调用此方法时,都会返回当前的nextHashCode
值,并将其增加HASH_INCREMENT
。
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
这里的getAndAdd(int delta)
方法实际上是Unsafe
类的getAndAddInt
方法,用于执行原子的整数加法操作。
哈希码的生成和管理确保了线程局部变量在ThreadLocalMap
中的唯一性和可访问性。
主要方法
get
方法
get
方法用于检索当前线程的线程局部变量的值。
查看代码
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();
}
以下是ThreadLocal
的get
方法的执行流程。
获取当前线程:
get
方法首先调用Thread.currentThread()
来获取当前正在执行的线程。获取线程的ThreadLocalMap:通过调用
getMap(t)
方法,获取当前线程的threadLocals
变量,这是一个ThreadLocalMap
类型的变量。每个线程存储着一个ThreadLocalMap
引用。javaThreadLocalMap getMap(Thread t) { return t.threadLocals; }
从ThreadLocalMap中获取条目:如果
ThreadLocalMap
不为null
,则调用map.getEntry(this)
,其中this
是当前的ThreadLocal
实例。这个方法尝试获取与当前ThreadLocal
实例关联的条目(Entry
)。javaprivate Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.refersTo(key)) return e; else return getEntryAfterMiss(key, i, e); }
检查条目:如果找到了条目(
Entry
),则检查条目的键是否确实是指向当前的ThreadLocal
实例。如果是,将条目的值(e.value
)转型为泛型类型T
并返回。处理未命中情况:如果在
ThreadLocalMap
中没有找到对应的条目,或者条目已经无效(即键为null
),则会调用getEntryAfterMiss
方法。这个方法会遍历哈希表,以处理哈希冲突的情况,并且在这个过程中清理掉无效的条目。查看代码
javaprivate Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { if (e.refersTo(key)) return e; if (e.refersTo(null)) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
清理无效条目:如果
getEntryAfterMiss
方法在遍历过程中遇到了无效的条目(键为null
),则会调用expungeStaleEntry
方法来清理这个无效的条目,并且可能重新哈希其他条目。查看代码
javaprivate int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter 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; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
设置初始值:如果
ThreadLocalMap
为null
或者没有找到对应的条目,get
方法会调用setInitialValue()
。这个方法首先调用initialValue()
来获取初始值,这个方法默认返回null
,但可以在子类中被重写以提供非null
的初始值。查看代码
javaprivate T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); } else { createMap(t, value); } if (this instanceof TerminatingThreadLocal) { TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this); } return value; } protected T initialValue() { return null; }
创建ThreadLocalMap:如果当前线程的
threadLocals
变量为null
,则调用createMap
方法来创建一个新的ThreadLocalMap
,并将初始值存入。javavoid createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
返回初始值:
setInitialValue
方法最后返回初始值,这个值也会被get
方法返回。
在整个过程中,ThreadLocal
保证了每个线程都有自己的局部变量副本,不会与其他线程共享,从而实现了线程隔离。
set
方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
以下是ThreadLocal
的set
方法的执行流程:
获取当前线程:
set
方法首先调用Thread.currentThread()
来获取当前正在执行的线程。获取线程的ThreadLocalMap:通过调用
getMap(t)
方法,获取当前线程的threadLocals
变量,这是一个ThreadLocalMap
类型的变量。检查ThreadLocalMap是否为空:
- 如果
ThreadLocalMap
不为null
,则继续执行。 - 如果
ThreadLocalMap
为null
,则调用createMap(t, value)
创建一个新的ThreadLocalMap
,并将当前ThreadLocal
实例和要设置的值作为初始键值对存入。
- 如果
设置值:调用
map.set(this, value)
来设置值。查看代码
javaprivate 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; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.refersTo(key)) { e.value = value; return; } if (e.refersTo(null)) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
遍历哈希表:
从计算出的索引位置开始,如果当前位置的条目(
Entry
)不为null
,则进行以下检查:检查条目是否匹配:如果当前条目的键(
ThreadLocal
实例)与this
相等,则更新该条目的值为新值,并结束方法。检查条目是否无效:如果当前条目的键为
null
(表示条目无效),则调用replaceStaleEntry
方法来替换无效的条目,并结束方法。查看代码
javaprivate 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; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.refersTo(null)) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { // 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. if (e.refersTo(key)) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists 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. if (e.refersTo(null) && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot 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); }
如果以上都不满足,则使用
nextIndex
方法找到下一个索引位置,继续遍历。
添加新条目:如果在哈希表中没有找到匹配的条目,则创建一个新的
Entry
对象,并将其放入计算出的索引位置。清理哈希表:在添加新条目后,调用
cleanSomeSlots
方法来清理哈希表中的无效条目。这个方法会遍历哈希表的一部分,清理遇到的无效条目。查看代码
javaprivate 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.refersTo(null)) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; }
检查是否需要重新哈希:
如果
cleanSomeSlots
方法没有清理任何条目,并且当前哈希表的大小超过了阈值,则调用rehash
方法。javaprivate void rehash() { expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis if (size >= threshold - threshold / 4) resize(); }
rehash
方法首先调用expungeStaleEntries
来清理所有的无效条目。javaprivate void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; for (int j = 0; j < len; j++) { Entry e = tab[j]; if (e != null && e.refersTo(null)) expungeStaleEntry(j); } }
如果清理后的大小仍然大于阈值减去阈值的一部分(通常是阈值减去四分之一),则调用
resize
方法来扩容哈希表。查看代码
javaprivate 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 { int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }
该
resize
方法将存储数据的数组扩容为原来长度的两倍,同时重新计算每个元素在新数组中的位置,并更新map的大小和扩容阈值。
结束:
set
方法执行完毕,新的值已经存储在当前线程的ThreadLocalMap
中。
remove
方法
以下是remove
方法的执行流程:
获取当前线程的ThreadLocalMap:
remove
方法首先通过调用getMap(Thread.currentThread())
来获取当前线程的ThreadLocalMap
实例。javapublic void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) { m.remove(this); } }
检查ThreadLocalMap是否为空:如果
ThreadLocalMap
不为null
,则继续执行删除操作;如果为null
,则方法结束,因为没有可删除的条目。删除条目:调用
m.remove(this)
,其中this
是当前的ThreadLocal
实例。以下是remove
方法的详细步骤:查看代码
javaprivate 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.refersTo(key)) { e.clear(); expungeStaleEntry(i); return; } } }
- 计算哈希索引:通过
key.threadLocalHashCode & (len-1)
计算当前ThreadLocal
实例的哈希索引,其中len
是哈希表的长度。 - 遍历哈希表:从计算出的索引位置开始,如果当前位置的条目(
Entry
)不为null
,则进行以下检查:检查条目是否匹配:通过调用
e.refersTo(key)
来检查当前条目的键是否与ThreadLocal
实例相等。refersTo
方法最终会调用到refersTo0
这个本地方法,它是一个@IntrinsicCandidate
,表示它可能被JVM内部优化。javapublic final boolean refersTo(T obj) { return refersToImpl(obj); } boolean refersToImpl(T obj) { return refersTo0(obj); } @IntrinsicCandidate private native boolean refersTo0(Object o);
如果条目匹配(即找到了对应的
Entry
),则调用e.clear()
来清除条目的值,然后调用expungeStaleEntry(i)
来清理当前索引位置的条目,并结束方法。如果条目不匹配,则使用
nextIndex
方法找到下一个索引位置,继续遍历。
- 计算哈希索引:通过
清理无效条目:
expungeStaleEntry(i)
方法会将当前索引位置的条目置为null
,并重新哈希后续的条目,同时清理掉所有遇到的无效条目(键为null
的条目)。结束:如果遍历完整个哈希表都没有找到匹配的条目,则
remove
方法结束,没有进行任何操作。
哈希冲突解决
在Java的ThreadLocal
实现中,哈希冲突的解决主要依赖于哈希表的动态调整和重哈希机制。当两个不同的ThreadLocal
实例通过哈希函数映射到同一个哈希桶(bucket)时,就会发生哈希冲突。
ThreadLocal
使用了一个开放定址法(Open Addressing)的策略来解决哈希冲突。
- 使用哈希码与桶数组大小取模:
ThreadLocal
使用哈希码与桶数组大小取模的方式来确定哈希桶的位置。这种方法可以有效减少哈希冲突,因为不同的键通过取模操作通常会分散到不同的桶中。 - 线性探测:当发生哈希冲突时,
ThreadLocal
使用线性探测(Linear Probing)的方式来解决冲突。这意味着如果在目标桶中发现有其他元素,就会检查下一个桶,如果仍然被占用,就继续检查下一个桶,直到找到一个空桶为止。 - 重哈希:当哈希表中的条目数达到阈值时,
ThreadLocal
会触发重哈希操作。重哈希会创建一个新的更大的哈希表,并将原有的键值对重新插入到新的哈希表中。这个过程有助于进一步减少哈希冲突。 - 动态调整桶数组大小:
ThreadLocal
的哈希表是一个动态调整大小的数组。当重哈希操作发生时,数组的大小会翻倍。这种策略有助于在哈希表变得过于拥挤时,通过增加空间来减少冲突。 - 阈值调整:
ThreadLocal
的阈值(threshold)是一个控制重哈希操作的关键参数。当哈希表中的条目数超过这个阈值时,就会触发重哈希。这个阈值通常设置为哈希表大小的某个比例,例如2/3
。
内存泄漏
ThreadLocalMap
是 ThreadLocal
的静态内部类,每个线程对象都持有一个 ThreadLocalMap
实例。
当线程访问 ThreadLocal
变量时,实际上是访问线程自己的 ThreadLocalMap
,这个映射表存储了 ThreadLocal
与其对应的数据。
ThreadLocalMap
中的数据是以 Entry
对象的形式存储的,每个 Entry
对象包含一个 ThreadLocal
对象的弱引用作为键(Key)和一个具体值(Value)。
Entry
对象中的键是 ThreadLocal
对象的弱引用。弱引用的特点是,它不会阻止垃圾回收器回收其引用的对象。
当 ThreadLocal
对象不再有强引用指向它时,垃圾回收器可能会在任何时候回收这个 ThreadLocal
对象。
由于 ThreadLocalMap
中的键是弱引用,一旦 ThreadLocal
对象被回收,Entry
中的键将变为 null
,但值仍然存在。
即使 ThreadLocal
对象被回收,ThreadLocalMap
中的 Entry
对象仍然被线程对象强引用,因此 Entry
对象及其值不会立即被回收。
如果线程对象长时间存活(例如,线程池中的线程),那么这些无用的 Entry
对象及其值将一直占用内存,导致内存泄漏。
如果线程是短暂存在的,那么即使发生内存泄漏,线程结束后,其占用的内存也会被回收。
如果线程长时间存在(如线程池中的线程),且没有正确清理 ThreadLocal
,则可能导致内存泄漏。
在线程池中,线程可能会被重复使用,这意味着线程的生命周期可能会比单个任务的生命周期长。
在 ThreadLocal
的 get
、set
和 remove
方法中,ThreadLocalMap
会尝试清理那些键为 null
的 Entry
对象。
这种清理是有限的,因为它依赖于对这些方法的调用频率,并不能保证立即清理所有的无效 Entry
。
总结来说,内存泄漏的过程如下:
- 线程创建: 当线程创建时,每个线程都会有一个
ThreadLocalMap
。 - 访问
ThreadLocal
: 当线程访问一个ThreadLocal
变量时,会在ThreadLocalMap
中创建一个Entry
对象,其中key
是ThreadLocal
实例的弱引用,value
是ThreadLocal
变量的值。 - GC触发: 如果没有任何强引用指向
ThreadLocal
实例,那么ThreadLocal
实例会被垃圾回收,Entry
对象的key
将变为null
。 - 内存泄漏: 此时,虽然
Entry
的key
已经为null
,但由于ThreadLocalMap
的存在,Entry
的value
仍然保持不变。如果线程继续存活,那么这个Entry
将不会被垃圾回收,导致内存泄漏。