ThreadLocal
简单随手总结一下-后面细节逻辑慢慢补充 ~~~
Q : 为什么能够保证线程安全的核心本质就是 每一个线程内部都维护了一个ThreadLocalMap 的示列。细节太多,看源码更加清晰
ThreadLocalMap 是一个一个的Entry类型的数组,key 就是当前的ThreadLocal 对象实例,value 就是我们自定义的数据,当然通常我们的ThreadLocal 都是 单列的,为什么?这也就保证了我们同一个线程之前不同组件中 能够共享数据的原因,如果同一个线程,每次调用都会产生新的Threadlocal 实例,从而导致了我们访问的不是同一个对应Entry 对象,访问的数据也不同,若你提前不没有存储,拿到的会是NUll、
Q: ThreadLocal 是怎么存储数据的,其实也是类似map 一样, 拿到我们key 的hahscode & 数组的大小 -1
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();
}其实这里大致做了以下操作:首先获取到 索引位置后,判断当前的key 是否为null 如果是之间 new entry 数组存放,如果不为null
就会遍历数组,直到遇到的entry 不为 null ,然后判断当前的entry 的 key 是否是同一个对象,是就覆盖返回,不是的话,会判断是否是僵尸条目,就是内存泄漏现象,entry 不为 null key 为null value 不为null ,它会去使用这个位置,同时 执行replaceStaleEntry 方法继续去遍历发现是否有僵尸条目直到遇到null
它并不会删除所有的僵尸条目,而是主要清理从 “当前发现的僵尸条目” 开始,到 “遇到第一个 null” 为止的这一段探测链上的僵尸条目。
还是hash 冲突是怎么解决?
不是map 那种 通过 链表 和 红黑树那样的,而是通过 线性探测,就是从冲突的 位置开始循环向后去找空的位置,然后存放
当然 删除的时候,remove 也并不是单纯吧指定的threadlocal 当前实例null 删除,这样子会破坏 线性探测链的,会影响其他key的删除的,所以在删除的时候,也是会去遍历直到null 过程中遍历到的entry 如果是僵尸条目 也会去删除的,然后就是正常的entry 会去判断他真正的索引是否等于当前的i 如果不是,说明是通过冲突 然后线性链表遍历找到的,然后就会去吧当前位置指控,到指定的真正索引位置开始找到null 的去存放
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;
}
}
}扩容的过程
resize() 方法的大致流程如下:
创建新数组:创建一个新的、容量是原数组两倍的新数组。
重新哈希(Rehashing):遍历旧数组中的所有非空
Entry。重新插入:
对于每个
Entry,它会根据其key(即ThreadLocal实例)重新计算在新数组中的位置。然后,它会使用线性探测法将这个
Entry插入到新数组中。
替换引用:将
ThreadLocalMap的内部数组引用指向这个新数组,并更新新的threshold。
remove() 方法也不是一次性清理整个数组中的所有僵尸条目,它的清理范围同样是增量式的,主要集中在当前操作的 “探测链” 上。set 也是一样的 都是 过程去清理一些漏网之鱼
为什么 remove() 不清理整个数组?
原因和 set() 方法不这么做是一样的,都是基于性能权衡:
避免性能开销:遍历整个数组进行全面清理是一个 O (n) 的耗时操作。如果每次调用
remove()都这么做,会严重影响ThreadLocal的性能,尤其是在高并发和ThreadLocal数量较多的场景下。清理最相关的区域:
remove()操作本身就表明线程正在访问这条探测链。清理这个区域内的僵尸条目,能最直接地优化后续对同一探测链的get()或set()操作的性能。责任边界:
remove()的主要职责是 “删除指定的ThreadLocal实例”,清理僵尸条目是一个附加的、优化性的 “副作用”。将全面清理的责任强加给它,会模糊其核心功能,并带来不必要的性能成本。
总结
ThreadLocalMap 的设计者在清理僵尸条目(以防止内存泄漏)这个问题上,采取了一种非常务实的策略:
不追求 “一劳永逸”:不提供一个遍历整个数组的
cleanupAll()之类的方法,因为这会鼓励滥用,导致性能问题。而是 “见缝插针”:在
get()、set()、remove()这三个最常用的操作中,顺便对当前访问的探测链进行清理。分散成本,提升热点性能:这样做可以将清理的成本均匀地分散到多次日常操作中,避免了单次操作的性能尖峰,同时保证了程序热点路径(hot path)的性能。