|
8 | 8 |
|
9 | 9 | 对于`ThreadLocal`,大家的第一反应可能是很简单呀,线程的变量副本,每个线程隔离。那这里有几个问题大家可以思考一下: |
10 | 10 |
|
11 | | -- `ThreadLocal`的 key 是**弱引用**,那么在 `ThreadLocal`.get()的时候,发生**GC**之后,key 是否为**null**? |
| 11 | +- `ThreadLocal`的 key 是**弱引用**,那么在 `ThreadLocal.get()`的时候,发生**GC**之后,key 是否为**null**? |
12 | 12 | - `ThreadLocal`中`ThreadLocalMap`的**数据结构**? |
13 | 13 | - `ThreadLocalMap`的**Hash 算法**? |
14 | 14 | - `ThreadLocalMap`中**Hash 冲突**如何解决? |
@@ -80,7 +80,7 @@ size: 0 |
80 | 80 |
|
81 | 81 | ### GC 之后 key 是否为 null? |
82 | 82 |
|
83 | | -回应开头的那个问题, `ThreadLocal` 的`key`是弱引用,那么在`ThreadLocal.get()`的时候,发生`GC`之后,`key`是否是`null`? |
| 83 | +回应开头的那个问题, `ThreadLocal` 的`key`是弱引用,那么在`ThreadLocal.get()`的时候,发生`GC`之后,`key`是否是`null`? |
84 | 84 |
|
85 | 85 | 为了搞清楚这个问题,我们需要搞清楚`Java`的**四种引用类型**: |
86 | 86 |
|
@@ -241,15 +241,15 @@ public class ThreadLocal<T> { |
241 | 241 |
|
242 | 242 | > **注明:** 下面所有示例图中,**绿色块**`Entry`代表**正常数据**,**灰色块**代表`Entry`的`key`值为`null`,**已被垃圾回收**。**白色块**表示`Entry`为`null`。 |
243 | 243 |
|
244 | | -虽然`ThreadLocalMap`中使用了**黄金分割数来**作为`hash`计算因子,大大减少了`Hash`冲突的概率,但是仍然会存在冲突。 |
| 244 | +虽然`ThreadLocalMap`中使用了**黄金分割数**来作为`hash`计算因子,大大减少了`Hash`冲突的概率,但是仍然会存在冲突。 |
245 | 245 |
|
246 | 246 | `HashMap`中解决冲突的方法是在数组上构造一个**链表**结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成**红黑树**。 |
247 | 247 |
|
248 | 248 | 而 `ThreadLocalMap` 中并没有链表结构,所以这里不能使用 `HashMap` 解决冲突的方式了。 |
249 | 249 |
|
250 | 250 |  |
251 | 251 |
|
252 | | -如上图所示,如果我们插入一个`value=27`的数据,通过 `hash` 计算后应该落入第 4 个槽位中,而槽位 4 已经有了 `Entry` 数据。 |
| 252 | +如上图所示,如果我们插入一个`value=27`的数据,通过 `hash` 计算后应该落入槽位 4 中,而槽位 4 已经有了 `Entry` 数据。 |
253 | 253 |
|
254 | 254 | 此时就会线性向后查找,一直找到 `Entry` 为 `null` 的槽位才会停止查找,将当前元素放入此槽位中。当然迭代过程中还有其他的情况,比如遇到了 `Entry` 不为 `null` 且 `key` 值相等的情况,还有 `Entry` 中的 `key` 值为 `null` 的情况等等都会有不同的处理,后面会一一详细讲解。 |
255 | 255 |
|
@@ -261,7 +261,7 @@ public class ThreadLocal<T> { |
261 | 261 |
|
262 | 262 | 看完了`ThreadLocal` **hash 算法**后,我们再来看`set`是如何实现的。 |
263 | 263 |
|
264 | | -往`ThreadLocalMap`中`set`数据(**新增**或者**更新**数据)分为好几种情况,针对不同的情况我们画图来说说明。 |
| 264 | +往`ThreadLocalMap`中`set`数据(**新增**或者**更新**数据)分为好几种情况,针对不同的情况我们画图来说明。 |
265 | 265 |
|
266 | 266 | **第一种情况:** 通过`hash`计算后的槽位对应的`Entry`数据为空: |
267 | 267 |
|
@@ -281,7 +281,7 @@ public class ThreadLocal<T> { |
281 | 281 |
|
282 | 282 | 遍历散列数组,线性往后查找,如果找到`Entry`为`null`的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了**key 值相等**的数据,直接更新即可。 |
283 | 283 |
|
284 | | -**第四种情况:** 槽位数据不为空,往后遍历过程中,在找到`Entry`为`null`的槽位之前,遇到`key`过期的`Entry`,如下图,往后遍历过程中,一到了`index=7`的槽位数据`Entry`的`key=null`: |
| 284 | +**第四种情况:** 槽位数据不为空,往后遍历过程中,在找到`Entry`为`null`的槽位之前,遇到`key`过期的`Entry`,如下图,往后遍历过程中,遇到了`index=7`的槽位数据`Entry`的`key=null`: |
285 | 285 |
|
286 | 286 |  |
287 | 287 |
|
@@ -299,7 +299,7 @@ public class ThreadLocal<T> { |
299 | 299 |
|
300 | 300 | 上面向前迭代的操作是为了更新探测清理过期数据的起始下标`slotToExpunge`的值,这个值在后面会讲解,它是用来判断当前过期槽位`staleSlot`之前是否还有过期元素。 |
301 | 301 |
|
302 | | -接着开始以`staleSlot`位置(index=7)向后迭代,**如果找到了相同 key 值的 Entry 数据:** |
| 302 | +接着开始以`staleSlot`位置(`index=7`)向后迭代,**如果找到了相同 key 值的 Entry 数据:** |
303 | 303 |
|
304 | 304 |  |
305 | 305 |
|
@@ -383,15 +383,15 @@ private static int prevIndex(int i, int len) { |
383 | 383 | 接着看剩下`for`循环中的逻辑: |
384 | 384 |
|
385 | 385 | 1. 遍历当前`key`值对应的桶中`Entry`数据为空,这说明散列数组这里没有数据冲突,跳出`for`循环,直接`set`数据到对应的桶中 |
386 | | -2. 如果`key`值对应的桶中`Entry`数据不为空 |
387 | | - 2.1 如果`k = key`,说明当前`set`操作是一个替换操作,做替换逻辑,直接返回 |
388 | | - 2.2 如果`key = null`,说明当前桶位置的`Entry`是过期数据,执行`replaceStaleEntry()`方法(核心方法),然后返回 |
389 | | -3. `for`循环执行完毕,继续往下执行说明向后迭代的过程中遇到了`entry`为`null`的情况 |
390 | | - 3.1 在`Entry`为`null`的桶中创建一个新的`Entry`对象 |
391 | | - 3.2 执行`++size`操作 |
392 | | -4. 调用`cleanSomeSlots()`做一次启发式清理工作,清理散列数组中`Entry`的`key`过期的数据 |
393 | | - 4.1 如果清理工作完成后,未清理到任何数据,且`size`超过了阈值(数组长度的 2/3),进行`rehash()`操作 |
394 | | - 4.2 `rehash()`中会先进行一轮探测式清理,清理过期`key`,清理完成后如果**size >= threshold - threshold / 4**,就会执行真正的扩容逻辑(扩容逻辑往后看) |
| 386 | +2. 如果`key`值对应的桶中`Entry`数据不为空 |
| 387 | + 2.1 如果`k = key`,说明当前`set`操作是一个替换操作,做替换逻辑,直接返回 |
| 388 | + 2.2 如果`key = null`,说明当前桶位置的`Entry`是过期数据,执行`replaceStaleEntry()`方法(核心方法),然后返回 |
| 389 | +3. `for`循环执行完毕,继续往下执行说明向后迭代的过程中遇到了`entry`为`null`的情况 |
| 390 | + 3.1 在`Entry`为`null`的桶中创建一个新的`Entry`对象 |
| 391 | + 3.2 执行`++size`操作 |
| 392 | +4. 调用`cleanSomeSlots()`做一次启发式清理工作,清理散列数组中`Entry`的`key`过期的数据 |
| 393 | + 4.1 如果清理工作完成后,未清理到任何数据,且`size`超过了阈值(数组长度的 2/3),进行`rehash()`操作 |
| 394 | + 4.2 `rehash()`中会先进行一轮探测式清理,清理过期`key`,清理完成后如果**size >= threshold - threshold / 4**,就会执行真正的扩容逻辑(扩容逻辑往后看) |
395 | 395 |
|
396 | 396 | 接着重点看下`replaceStaleEntry()`方法,`replaceStaleEntry()`方法提供替换过期数据的功能,我们可以对应上面**第四种情况**的原理图来再回顾下,具体代码如下: |
397 | 397 |
|
@@ -510,7 +510,7 @@ if (slotToExpunge != staleSlot) |
510 | 510 |
|
511 | 511 | 如果再有其他数据`set`到`map`中,就会触发**探测式清理**操作。 |
512 | 512 |
|
513 | | -如上图,执行**探测式清理**后,`index=5`的数据被清理掉,继续往后迭代,到`index=7`的元素时,经过`rehash`后发现该元素正确的`index=4`,而此位置已经已经有了数据,往后查找离`index=4`最近的`Entry=null`的节点(刚被探测式清理掉的数据:index=5),找到后移动`index= 7`的数据到`index=5`中,此时桶的位置离正确的位置`index=4`更近了。 |
| 513 | +如上图,执行**探测式清理**后,`index=5`的数据被清理掉,继续往后迭代,到`index=7`的元素时,经过`rehash`后发现该元素正确的`index=4`,而此位置已经有了数据,往后查找离`index=4`最近的`Entry=null`的节点(刚被探测式清理掉的数据:`index=5`),找到后移动`index= 7`的数据到`index=5`中,此时桶的位置离正确的位置`index=4`更近了。 |
514 | 514 |
|
515 | 515 | 经过一轮探测式清理后,`key`过期的数据会被清理掉,没过期的数据经过`rehash`重定位后所处的桶位置理论上更接近`i= key.hashCode & (tab.len - 1)`的位置。这种优化会提高整个散列表查询性能。 |
516 | 516 |
|
@@ -627,7 +627,7 @@ private void expungeStaleEntries() { |
627 | 627 | } |
628 | 628 | ``` |
629 | 629 |
|
630 | | -这里首先是会进行探测式清理工作,从`table`的起始位置往后清理,上面有分析清理的详细流程。清理完成之后,`table`中可能有一些`key`为`null`的`Entry`数据被清理掉,所以此时通过判断`size >= threshold - threshold / 4` 也就是`size >= threshold* 3/4` 来决定是否扩容。 |
| 630 | +这里首先是会进行探测式清理工作,从`table`的起始位置往后清理,上面有分析清理的详细流程。清理完成之后,`table`中可能有一些`key`为`null`的`Entry`数据被清理掉,所以此时通过判断`size >= threshold - threshold / 4` 也就是`size >= threshold * 3/4` 来决定是否扩容。 |
631 | 631 |
|
632 | 632 | 我们还记得上面进行`rehash()`的阈值是`size >= threshold`,所以当面试官套路我们`ThreadLocalMap`扩容机制的时候 我们一定要说清楚这两个步骤: |
633 | 633 |
|
@@ -723,7 +723,7 @@ private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { |
723 | 723 |
|
724 | 724 | ### `ThreadLocalMap`过期 key 的启发式清理流程 |
725 | 725 |
|
726 | | -上面多次提及到`ThreadLocalMap`过期可以的两种清理方式:**探测式清理(expungeStaleEntry())**、**启发式清理(cleanSomeSlots())** |
| 726 | +上面多次提及到`ThreadLocalMap`过期key的两种清理方式:**探测式清理(expungeStaleEntry())**、**启发式清理(cleanSomeSlots())** |
727 | 727 |
|
728 | 728 | 探测式清理是以当前`Entry` 往后清理,遇到值为`null`则结束清理,属于**线性探测清理**。 |
729 | 729 |
|
|