网站运维公司有哪些优化网哪个牌子好
“三年前我也曾被这个技术点卡住… 你好,我是曾续缘。今天把踩坑经验转化成这份避坑指南,关注我,让我的弯路变成你的捷径🛣️”
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);}@Overrideprotected 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
引用。ThreadLocalMap getMap(Thread t) {return t.threadLocals; }
-
从ThreadLocalMap中获取条目:如果
ThreadLocalMap
不为null
,则调用map.getEntry(this)
,其中this
是当前的ThreadLocal
实例。这个方法尝试获取与当前ThreadLocal
实例关联的条目(Entry
)。private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.refersTo(key))return e;elsereturn getEntryAfterMiss(key, i, e); }
-
检查条目:如果找到了条目(
Entry
),则检查条目的键是否确实是指向当前的ThreadLocal
实例。如果是,将条目的值(e.value
)转型为泛型类型T
并返回。 -
处理未命中情况:如果在
ThreadLocalMap
中没有找到对应的条目,或者条目已经无效(即键为null
),则会调用getEntryAfterMiss
方法。这个方法会遍历哈希表,以处理哈希冲突的情况,并且在这个过程中清理掉无效的条目。private 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);elsei = nextIndex(i, len);e = tab[i];}return null; }
-
清理无效条目:如果
getEntryAfterMiss
方法在遍历过程中遇到了无效的条目(键为null
),则会调用expungeStaleEntry
方法来清理这个无效的条目,并且可能重新哈希其他条目。private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// expunge entry at staleSlottab[staleSlot].value = null;tab[staleSlot] = null;size--;// Rehash until we encounter nullEntry 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
的初始值。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);}if (this instanceof TerminatingThreadLocal) {TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);}return value; } protected T initialValue() {return null; }
-
创建ThreadLocalMap:如果当前线程的
threadLocals
变量为null
,则调用createMap
方法来创建一个新的ThreadLocalMap
,并将初始值存入。void 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)
来设置值。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;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
方法来替换无效的条目,并结束方法。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;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 firstfor (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 existsif (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 slottab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// If there are any other stale entries in run, expunge themif (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
-
如果以上都不满足,则使用
nextIndex
方法找到下一个索引位置,继续遍历。
-
-
添加新条目:如果在哈希表中没有找到匹配的条目,则创建一个新的
Entry
对象,并将其放入计算出的索引位置。 -
清理哈希表:在添加新条目后,调用
cleanSomeSlots
方法来清理哈希表中的无效条目。这个方法会遍历哈希表的一部分,清理遇到的无效条目。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.refersTo(null)) {n = len;removed = true;i = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0);return removed; }
-
检查是否需要重新哈希:
-
如果
cleanSomeSlots
方法没有清理任何条目,并且当前哈希表的大小超过了阈值,则调用rehash
方法。private void rehash() {expungeStaleEntries();// Use lower threshold for doubling to avoid hysteresisif (size >= threshold - threshold / 4)resize(); }
-
rehash
方法首先调用expungeStaleEntries
来清理所有的无效条目。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.refersTo(null))expungeStaleEntry(j);} }
-
如果清理后的大小仍然大于阈值减去阈值的一部分(通常是阈值减去四分之一),则调用
resize
方法来扩容哈希表。private 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
实例。public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null) {m.remove(this);}}
-
检查ThreadLocalMap是否为空:如果
ThreadLocalMap
不为null
,则继续执行删除操作;如果为null
,则方法结束,因为没有可删除的条目。 -
删除条目:调用
m.remove(this)
,其中this
是当前的ThreadLocal
实例。以下是remove
方法的详细步骤: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.refersTo(key)) {e.clear();expungeStaleEntry(i);return;}} }
- 计算哈希索引:通过
key.threadLocalHashCode & (len-1)
计算当前ThreadLocal
实例的哈希索引,其中len
是哈希表的长度。 - 遍历哈希表:从计算出的索引位置开始,如果当前位置的条目(
Entry
)不为null
,则进行以下检查:-
检查条目是否匹配:通过调用
e.refersTo(key)
来检查当前条目的键是否与ThreadLocal
实例相等。refersTo
方法最终会调用到refersTo0
这个本地方法,它是一个@IntrinsicCandidate
,表示它可能被JVM内部优化。public 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
将不会被垃圾回收,导致内存泄漏。
t、
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
将不会被垃圾回收,导致内存泄漏。
参考文章:https://cengxuyuan.cn