Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 6842592

Browse files
Merge pull request Snailclimb#1364 from anaer/patch-21
Update 万字详解ThreadLocal关键字.md
2 parents cc5f281 + de32126 commit 6842592

File tree

1 file changed

+19
-19
lines changed

1 file changed

+19
-19
lines changed

‎docs/java/multi-thread/万字详解ThreadLocal关键字.md‎

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
对于`ThreadLocal`,大家的第一反应可能是很简单呀,线程的变量副本,每个线程隔离。那这里有几个问题大家可以思考一下:
1010

11-
- `ThreadLocal`的 key 是**弱引用**,那么在 `ThreadLocal`.get()的时候,发生**GC**之后,key 是否为**null**?
11+
- `ThreadLocal`的 key 是**弱引用**,那么在 `ThreadLocal.get()`的时候,发生**GC**之后,key 是否为**null**?
1212
- `ThreadLocal``ThreadLocalMap`**数据结构**?
1313
- `ThreadLocalMap`**Hash 算法**?
1414
- `ThreadLocalMap`**Hash 冲突**如何解决?
@@ -80,7 +80,7 @@ size: 0
8080

8181
### GC 之后 key 是否为 null?
8282

83-
回应开头的那个问题, `ThreadLocal` 的`key`是弱引用,那么在`ThreadLocal.get()`的时候,发生`GC`之后,`key`是否是`null`?
83+
回应开头的那个问题, `ThreadLocal` 的`key`是弱引用,那么在`ThreadLocal.get()`的时候,发生`GC`之后,`key`是否是`null`?
8484

8585
为了搞清楚这个问题,我们需要搞清楚`Java`的**四种引用类型**:
8686

@@ -241,15 +241,15 @@ public class ThreadLocal<T> {
241241

242242
> **注明:** 下面所有示例图中,**绿色块**`Entry`代表**正常数据**,**灰色块**代表`Entry`的`key`值为`null`,**已被垃圾回收****白色块**表示`Entry`为`null`。
243243

244-
虽然`ThreadLocalMap`中使用了**黄金分割数来**作为`hash`计算因子,大大减少了`Hash`冲突的概率,但是仍然会存在冲突。
244+
虽然`ThreadLocalMap`中使用了**黄金分割数**来作为`hash`计算因子,大大减少了`Hash`冲突的概率,但是仍然会存在冲突。
245245

246246
`HashMap`中解决冲突的方法是在数组上构造一个**链表**结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成**红黑树**
247247

248248
而 `ThreadLocalMap` 中并没有链表结构,所以这里不能使用 `HashMap` 解决冲突的方式了。
249249

250250
![](./images/thread-local/7.png)
251251

252-
如上图所示,如果我们插入一个`value=27`的数据,通过 `hash` 计算后应该落入第 4 个槽位中,而槽位 4 已经有了 `Entry` 数据。
252+
如上图所示,如果我们插入一个`value=27`的数据,通过 `hash` 计算后应该落入槽位 4 ,而槽位 4 已经有了 `Entry` 数据。
253253

254254
此时就会线性向后查找,一直找到 `Entry` 为 `null` 的槽位才会停止查找,将当前元素放入此槽位中。当然迭代过程中还有其他的情况,比如遇到了 `Entry` 不为 `null` 且 `key` 值相等的情况,还有 `Entry` 中的 `key` 值为 `null` 的情况等等都会有不同的处理,后面会一一详细讲解。
255255

@@ -261,7 +261,7 @@ public class ThreadLocal<T> {
261261

262262
看完了`ThreadLocal` **hash 算法**后,我们再来看`set`是如何实现的。
263263

264-
往`ThreadLocalMap`中`set`数据(**新增**或者**更新**数据)分为好几种情况,针对不同的情况我们画图来说说明
264+
往`ThreadLocalMap`中`set`数据(**新增**或者**更新**数据)分为好几种情况,针对不同的情况我们画图来说明
265265

266266
**第一种情况:** 通过`hash`计算后的槽位对应的`Entry`数据为空:
267267

@@ -281,7 +281,7 @@ public class ThreadLocal<T> {
281281

282282
遍历散列数组,线性往后查找,如果找到`Entry`为`null`的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了**key 值相等**的数据,直接更新即可。
283283

284-
**第四种情况:** 槽位数据不为空,往后遍历过程中,在找到`Entry`为`null`的槽位之前,遇到`key`过期的`Entry`,如下图,往后遍历过程中,一到了`index=7`的槽位数据`Entry`的`key=null`:
284+
**第四种情况:** 槽位数据不为空,往后遍历过程中,在找到`Entry`为`null`的槽位之前,遇到`key`过期的`Entry`,如下图,往后遍历过程中,遇到了`index=7`的槽位数据`Entry`的`key=null`:
285285

286286
![](./images/thread-local/12.png)
287287

@@ -299,7 +299,7 @@ public class ThreadLocal<T> {
299299

300300
上面向前迭代的操作是为了更新探测清理过期数据的起始下标`slotToExpunge`的值,这个值在后面会讲解,它是用来判断当前过期槽位`staleSlot`之前是否还有过期元素。
301301

302-
接着开始以`staleSlot`位置(index=7)向后迭代,**如果找到了相同 key 值的 Entry 数据:**
302+
接着开始以`staleSlot`位置(`index=7`)向后迭代,**如果找到了相同 key 值的 Entry 数据:**
303303

304304
![](./images/thread-local/14.png)
305305

@@ -383,15 +383,15 @@ private static int prevIndex(int i, int len) {
383383
接着看剩下`for`循环中的逻辑:
384384

385385
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**,就会执行真正的扩容逻辑(扩容逻辑往后看)
395395

396396
接着重点看下`replaceStaleEntry()`方法,`replaceStaleEntry()`方法提供替换过期数据的功能,我们可以对应上面**第四种情况**的原理图来再回顾下,具体代码如下:
397397

@@ -510,7 +510,7 @@ if (slotToExpunge != staleSlot)
510510

511511
如果再有其他数据`set`到`map`中,就会触发**探测式清理**操作。
512512

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`更近了。
514514

515515
经过一轮探测式清理后,`key`过期的数据会被清理掉,没过期的数据经过`rehash`重定位后所处的桶位置理论上更接近`i= key.hashCode & (tab.len - 1)`的位置。这种优化会提高整个散列表查询性能。
516516

@@ -627,7 +627,7 @@ private void expungeStaleEntries() {
627627
}
628628
```
629629

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` 来决定是否扩容。
631631

632632
我们还记得上面进行`rehash()`的阈值是`size >= threshold`,所以当面试官套路我们`ThreadLocalMap`扩容机制的时候 我们一定要说清楚这两个步骤:
633633

@@ -723,7 +723,7 @@ private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
723723

724724
### `ThreadLocalMap`过期 key 的启发式清理流程
725725

726-
上面多次提及到`ThreadLocalMap`过期可以的两种清理方式:**探测式清理(expungeStaleEntry())****启发式清理(cleanSomeSlots())**
726+
上面多次提及到`ThreadLocalMap`过期key的两种清理方式:**探测式清理(expungeStaleEntry())****启发式清理(cleanSomeSlots())**
727727

728728
探测式清理是以当前`Entry` 往后清理,遇到值为`null`则结束清理,属于**线性探测清理**
729729

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /