JDK源码系列:ThreadLocalMap到底是如何清理失效节点的?
创始人
2025-06-01 21:21:50

020d84227a867f99bcdfe9e4dab40773.jpeg

在前面的关于ThreadLocal的文章中提到了所谓的内存泄漏问题,同时也提到了ThreadLocalMap在某些场景下会主动清理坏掉的Entry来释放内存,要理解它是怎么做到的,就必须理解它是怎么解决哈希冲突的,尤其是“真删除”Entry后如何保证不影响后续Entry的查找问题

一、哈希冲突的解决:线性探测法

在线性探测法中插入和查找和修改都比较容易理解,难点在于删除动作,因为删除动作可能会影响后续节点查找的正确性,在业界有两种做法:

一种是假删除,在Entry上增加状态属性,将状态置为删除,优点是不需要移动后续Entry,缺点是不能完全释放Entry,同时在插入和查询时要增加状态判断逻辑,数据不够紧凑影响查找效率;如下:key3、key4、key5发生哈希冲突,删除key3节点,只需要把状态改为status_del即可,这样不会影响key4和key5的查询,把status_del当成存在的节点即可。

e94b08351846f9904cd7c157b4160f73.png

另一种是真删除,将Entry释放回收,同时移动后续Entry到正确的位置,优点就是数据紧凑查找效率高,缺点是影响删除的效率;

如下:key3、key4、key5发生哈希冲突,删除key3节点后,它的后续节点(key4,key5,key1)需要rehash,最终重新分布如下。

f39ac4fe8938c373a717790eabc47364.png

f8c741d44ad110eb22485f84b7430720.png

在ThreadLocalMap中使用的算法是“真删除”。

二、如何何时清理坏掉的Entry?

在get\set\remove方法中某些场景下会进行局部清理操作释放内存,在扩容之前也会进行全局清理操作(扩容方法在set方法中某种场景下调用),要想彻底弄明白还不是那么容易,下面是我对ThreadLocalMap的源码注释,感兴趣的同学可以瞧瞧。

public class ThreadLocal {/*** 为了让哈希码能均匀的分布在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);elsecreateMap(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);elsecreateMap(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 extends ThreadLocal {private final Supplier supplier;SuppliedThreadLocal(Supplier supplier) {this.supplier = Objects.requireNonNull(supplier);}@Overrideprotected 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 key = (ThreadLocal) 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)//清理过时EntryexpungeStaleEntry(i);else//线性探测:下一个元素i = nextIndex(i, len);e = tab[i];}//直到下一个元素为null,如果还没找到则返回nullreturn 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);//条目数量加1int 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,直到遇到nullreturn;}}}/*** 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<-beginfor (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 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;}/*** 主要作用:* 从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 hysteresisif (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> {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)清理操作;

相关内容

热门资讯

6寸照片尺寸(6寸照片尺寸大小... 今天给各位分享6寸照片尺寸的知识,其中也会对6寸照片尺寸大小图片进行解释,如果能碰巧解决你现在面临的...
全日制和在职的是什么意思 极速... 全日制和在职的是什么意思目录全日制和在职的是什么意思全日制和在职的是什么意思在职研究生是什么意思?和...
如何退货怎么寄回去,退货怎么寄... 如何退货怎么寄回去目录如何退货怎么寄回去退货怎么寄回去给商家买家怎样寄快递回商家退货要怎么寄回去,有...
葬爱家族怎么来的,当别人说你是... 葬爱家族怎么来的目录葬爱家族怎么来的当别人说你是葬爱家族你怎么回答?葬爱小龙是葬爱家族的吗? 有人说...
如何为孩子选购儿童脚踏车(儿童... 本篇文章极速百科给大家谈谈如何为孩子选购儿童脚踏车,以及儿童脚踏车什么牌子比较好对应的知识点,希望对...
学好高中语文的方法,怎样学好高... 学好高中语文的方法目录学好高中语文的方法怎样学好高中语文?如何学好高中语文如何学好高中语文学好高中语...
家用轿车哪款比较好?家用轿车排... 今天给各位分享家用轿车哪款比较好?家用轿车排行榜前十名2022的知识,其中也会对家用轿车哪款车最实用...
海尔电视怎么调出电视模式,海尔... 海尔电视怎么调出电视模式目录海尔电视怎么调出电视模式海尔电视怎么调出电视模式?海尔电视户户通怎么切换...
北京x7的优势有哪些hhhh8... 本篇文章极速百科给大家谈谈北京x7的优势有哪些hhhh88,以及北京x7好么对应的知识点,希望对各位...
日土县属于哪个市,西藏日土县是... 日土县属于哪个市目录日土县属于哪个市西藏日土县是哪个市?西藏包括哪些地方?日土县属于哪个市日土县属于...
排放标准国一到国六的符号是什么... 排放标准国一到国六的符号是什么目录排放标准国一到国六的符号是什么排放标准国一到国六的符号分别是什么?...
处暑下雨谚语,处暑下雨的谚语 ... 处暑下雨谚语目录处暑下雨谚语处暑下雨的谚语关于下雨的农谚或俗语有哪些?关于处暑的谚语大全处暑下雨谚语...
黄骅市邮编号码市多少,河北省黄... 黄骅市邮编号码市多少目录黄骅市邮编号码市多少河北省黄骅市羊二庄镇的邮编号码是多少黄骅市旧城镇的邮政编...
书记处书记是什么职位,书记处书... 书记处书记是什么职位目录书记处书记是什么职位书记处书记是干什么的学生会书记处是做什么的?主要负责什么...
埃及的货币叫什么,在埃及可以使... 埃及的货币叫什么目录埃及的货币叫什么在埃及可以使用什么货币,可以用美金吗?非洲各国的钱叫什么名称?埃...
怎样能快速学好英语,如何有效快... 怎样能快速学好英语目录怎样能快速学好英语如何有效快速学好英语怎么才能快速轻易学好英语?英语怎么才能快...
蔷薇属植物有哪些 极速百科网 ... 蔷薇属植物有哪些目录蔷薇属植物有哪些蔷薇属植物有哪些蔷薇属植物有哪些?在城市绿化中常用的有哪几种?蔷...
econ什么专业(eco是什么... 今天给各位分享econ什么专业的知识,其中也会对eco是什么专业进行解释,如果能碰巧解决你现在面临的...
oppo手机关不了机怎么办,o... 3. 检查电源键:确认电源键是否正常使用,可锁屏亮屏排除尝试。 5. 卸载近期下载的软件:若无...
快狗打车加入条件,加入快狗打车... 快狗打车加入条件目录快狗打车加入条件加入快狗打车需要什么条件?没有车可以加入快狗打车么?快狗打车加盟...
耳顺之年是指什么年龄(耳顺之年... 本篇文章极速百科给大家谈谈耳顺之年是指什么年龄,以及耳顺之年是指什么年龄段对应的知识点,希望对各位有...
海尔洗衣机出现e1故障怎么处理... 海尔洗衣机出现e1故障怎么处理目录海尔洗衣机出现e1故障怎么处理海尔洗衣机海尔玫瑰钻XQG50-E7...
三个龙念什么,龙加三个念什么 ... 三个龙念什么目录三个龙念什么龙加三个念什么三个龙念什么?三个龙的字怎么念?三个龙念什么 这个字...
8万左右轿车哪个省油五款热门8... 本篇文章极速百科给大家谈谈8万左右轿车哪个省油五款热门8万左右轿车介绍,以及8万左右比较省油的轿车对...
网上说的sm是啥意思,sm指的... 网上说的sm是啥意思目录什么是SM?sm指的什么分别指的什么 什么是SM?SM是指“性施虐癖”(S)...
怎么查自己电脑宽带的余额,联通... 怎么查自己电脑宽带的余额目录怎么查自己电脑宽带的余额联通怎么查宽带余额?怎么查电脑宽带里剩下多少钱如...
什么叫潮汐和潮汐能 极速百科网... 什么叫潮汐和潮汐能目录什么叫潮汐和潮汐能什么叫潮汐和潮汐能潮汐能是什么?什么叫潮汐和潮汐能什么叫潮汐...
借呗额度为什么会降低,支付宝借... 借呗额度为什么会降低目录借呗额度为什么会降低支付宝借呗降额原因有哪些蚂蚁借呗额度怎么降低了 蚂蚁借呗...
河南豫剧边玉洁多大了,河南豫剧... 河南豫剧边玉洁多大了目录河南豫剧边玉洁多大了河南豫剧边玉洁多大了(河南豫剧边玉洁的老公)边玉洁的介绍...
大轮毂薄胎和小轮毂厚胎哪个比较... 今天给各位分享大轮毂薄胎和小轮毂厚胎哪个比较好?总结一下它们各自...的知识,其中也会对汽车轮毂大轮...