在前面的关于ThreadLocal的文章中提到了所谓的内存泄漏问题,同时也提到了ThreadLocalMap在某些场景下会主动清理坏掉的Entry来释放内存,要理解它是怎么做到的,就必须理解它是怎么解决哈希冲突的,尤其是“真删除”Entry后如何保证不影响后续Entry的查找问题
一、哈希冲突的解决:线性探测法
在线性探测法中插入和查找和修改都比较容易理解,难点在于删除动作,因为删除动作可能会影响后续节点查找的正确性,在业界有两种做法:
一种是假删除,在Entry上增加状态属性,将状态置为删除,优点是不需要移动后续Entry,缺点是不能完全释放Entry,同时在插入和查询时要增加状态判断逻辑,数据不够紧凑影响查找效率;如下:key3、key4、key5发生哈希冲突,删除key3节点,只需要把状态改为status_del即可,这样不会影响key4和key5的查询,把status_del当成存在的节点即可。
另一种是真删除,将Entry释放回收,同时移动后续Entry到正确的位置,优点就是数据紧凑查找效率高,缺点是影响删除的效率;
如下:key3、key4、key5发生哈希冲突,删除key3节点后,它的后续节点(key4,key5,key1)需要rehash,最终重新分布如下。
在ThreadLocalMap中使用的算法是“真删除”。
二、如何何时清理坏掉的Entry?
在get\set\remove方法中某些场景下会进行局部清理操作释放内存,在扩容之前也会进行全局清理操作(扩容方法在set方法中某种场景下调用),要想彻底弄明白还不是那么容易,下面是我对ThreadLocalMap的源码注释,感兴趣的同学可以瞧瞧。
public class ThreadLocal<T> {
/**
* 为了让哈希码能均匀的分布在2的N次方的数组里
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 用来辅助生成哈希码的
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* ThreadLocal 实例作为ThreadLocalMap的key,它的哈希码来自于此处
*/
private final int threadLocalHashCode = nextHashCode();
/**
* 生成哈希码的方法
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
/**
* 创建可继承的ThreadLocalMap
* 在Thread构造方法中可能调用此方法
*/
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
/**
* 获取当前ThreadLocal实例关联当前线程的value
* 如果ThreadLocalMap还没有创建则会进行创建,并初始化节点值
*/
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();
}
/**
* ThreadLocalMap初始化
*/
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;
}
/**
* 设置值,如果map不存在则初始化map,并初始化节点值
*/
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实例关联的Entry
* 如果移除后调用get方法会导致Entry重新初始化,initialValue也会再次被执行。
* 除非在调用get之前调用了set操作(Entry仍然需要初始化,但initialValue不会执行)。
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* 可以看出ThreadLocalMap是直接挂在Thread对象上的
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 创建ThreadLocalMap并初始化第一个节点值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* InheritableThreadLocal中会覆盖此方法
*/
T childValue(T parentValue) {
throw new UnsupportedOperationException();
}
/**
* 获取初始化值的另一种方式
*/
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是一个为了维护线程本地值对象而定制的一个哈希map
* ThreadLocalMap访问权限是package级别
* 为了有助于及时清理又大存活时间又长的对象,对Entry中的key使用了弱引用
* 在扩容的情况下会清理过时的条目
*/
static class ThreadLocalMap {
/**
* 初始容量 --必须是2的N次方
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 数组,必要情况下可扩容
* 数组长度必须是2的N次方
*/
private Entry[] table;
/**
* 数组中Entry的数量
*/
private int size = 0;
/**
* 下次扩容的阈值
*/
private int threshold; // Default to 0
/**
* ThreadLocalMap初始化:数组初始化,firstKey数组下标计算,节点Entry初始化,Entry数量赋值,下次扩容阈值计算
*/
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(深克隆)
*/
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++;
}
}
}
}
/**
* 环形数组下标计算(下一个数组元素下标)
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 环形数组下标计算(上一个数组元素下标)
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
/**
* 扩容阈值的计算
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* 获取Entry
*/
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);
}
/**
* 存在哈希冲突情况下取值,线性探测法
*/
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)
//清理过时Entry
expungeStaleEntry(i);
else
//线性探测:下一个元素
i = nextIndex(i, len);
e = tab[i];
}
//直到下一个元素为null,如果还没找到则返回null
return null;
}
/**
* 设值
*/
private void set(ThreadLocal<?> key, Object value) {
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);
//条目数量加1
int sz = ++size;
//清理有限范围内的失效条目,如果找到了失效条目肯定会清理掉同时size--,这时一定不需要扩容。
//如果没有清理任何失效条目,则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();//清掉key的弱引用
expungeStaleEntry(i);//清理i位置的条目,后续条目rehash,直到遇到null
return;
}
}
}
/**
* 1、方法逻辑:先从当前位置A(过期的entry)向前遍历,直到遇到空entry,如果遇到过期entry,
* 则记录下标(指向距离null最近的失效条目),标记为清理开始位置。
* 从位置A向后遍历,直到遇到空entry,如果遇到key与当前ThreadLocal相等,那么填入value,
* 再把当前entry与位置A的entry换位,然后先执行expungeStaleEntry,再执行cleanSomeSlots清理过期entry;
* 如果没有找到key,那么就是遇到空entry了,直接new Entry并填入到位置A,然后同样先执行expungeStaleEntry,
* 再执行cleanSomeSlots方法清理。
* 2、如果不考虑清理的事情,只保证正确性的简单逻辑:向后遍历,在找到null节点之前,如果找到了key的旧节点,则填入value,
* 并与staleSlot进行指针交换(保证正确的核心逻辑);如果没找到key的旧节点则说明这是一个新key,直接放到staleSlot位置即可
* 3、核心思想:共3个指针,slotToExpunge指向最左侧失效节点,staleSlot始终指向中间位置不变,
* i可能会指向右侧失效节点,i作为辅助指针,某种场景下slotToExpunge会指向i的位置,
* 不管slotToExpunge在哪,它始终是清理的起始位置,从左向右进行启发式清理。
* 清理不是必须的,staleSlot与i的位置交换才是核心,它保证了正确性,当这是一个新key时,就不会有交换发生。
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
//向左查找失效节点,直到遇到null节点,用slotToExpunge记录最左侧失效节点的位置
//如下:slotToExpunge从begin位置开始向左找,最后会指向stale2
//null<-normal<-stale2<-normal<-stale1<-normal<-begin
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
//向后查找key所在节点(可能不存在))或者null节点
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//找到了key所在节点,和staleSlot交换位置
if (k == key) {
e.value = value;
//和staleSlot交换位置,staleSlot指向了好的节点,i指向了坏的节点
//交换完成后就不会存在错误了,后面的清理就是锦上添花的事情
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
//staleSlot指向的已经不是失效条目,所以目前是最左侧失效条目是slotToExpunge
//最右侧失效条目是i,如果slotToExpunge一直没动的话,也就是左侧没有失效条目,
//这时将slotToExpunge指向i才是正确的,也就是左右指针重叠
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//清理slotToExpunge指向的条目,并且进行"启发式清理"
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 (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
//没找到旧key说明这是一个新key,直接占用到staleSlot位置即可
//(这种情况下不清理任何失效节点也不会出错,后面的清理就是锦上添花的事情了)
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
//slotToExpunge发生了移动,说明找到了失效条目,则进行启发式清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* 删除一个指定位置的失效条目,并且重新hash后面的条目,直到遇到一个null条目。
* 基于 "哈希冲突线性探测法的真删除原理"
*/
private 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;
}
/**
* 主要作用:
* 从i位置开始搜索,如果在log2n个连续节点内发现了失效条目(key=null,value<>null)则进行清理失效条目,
* 并且重置n的值为len,如此循环,直到连续log2n个节点都是正常节点时才会跳出while。
* (和expungeStaleEntry(i)的区别就是它的清理范围更长,只要在log2n个连续节点有失效条目,
* 那么你的清理范围就可以一直延长,极端情况下可能会清理整个数组,也可能没有清理掉任何节点)
*/
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的值,这样就可以继续向后查找最多log2n个节点,可循环
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ((n >>>= 1) != 0);//不断的无符号右移,直到n = 0。如果连续log2n个节点都是正常节点则跳出循环
return removed;
}
/**
* 重新哈希所有条目:1、清理失效的条目 2、如果清理后还是达到了扩容阈值,则进行翻倍扩容
*/
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid 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;
}
/**
* 删除数组中所有失效条目:遵守线性探测法删除原理
*/
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);
}
}
/**
* Entry节点
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
三、总结
1、ThreadLocalMap使用了“线性探测法”来解决哈希冲突;
2、ThreadLocalMap使用了“真删除”来去除正常remove的节点或者坏掉的节点(key引用为null);
3、ThreadLocalMap真删除坏掉的节点后,会rehash后面的节点,直到遇到null Entry为止,这就是expungeStaleEntry(i)的逻辑;
4、get操作中,如果遇到了哈希冲突,就会进行线性探测法查找,过程中遇到了失效节点就会发生expungeStaleEntry(j)清理,直到遇到null节点终止;
5、set操作中,某些场景下会进行复杂的replaceStaleEntry(key, value, i)和cleanSomeSlots(expungeStaleEntry(slotToExpunge), len)释放内存操作.
简单理解就是将新值放到“最优”的位置,方便高效查询,如果是一个新增操作就直接放在最优位置了,如果是一个更新操作,就需要找到“旧值”所在的位置,并进行“交换”操作,这就可以保证最基本的正确性了(map中不能存在两个相同key的条目)。
至于清理操作则是锦上添花的事情了,一方面为了及时清理失效节点释放内存,另一方面可以避免不必要的扩容。
在没有回收且占用了新的null节点场景下set后 size++很可能达到扩容的阈值,这时候会进行局部清理操作(cleanSomeSlots)后可能会size--,这就避免了扩容操作。
如果没有回收任何失效的条目则会进行全局rehash操作,在rehash操作中会先进行全局回收(遍历所有条目),如果size还是超越了阈值则会进行翻倍扩容操作;
6、remove操作中,会先找到对应Entry,然后回收key的指针,最后进行expungeStaleEntry(i)清理操作;