
在前面的关于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 {/*** 为了让哈希码能均匀的分布在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 extends T> supplier;SuppliedThreadLocal(Supplier extends T> 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
三、总结
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)清理操作;