Skip to content

Commit a6d3421

Browse files
authored
Merge pull request crossoverJie#70 from crossoverJie/fix
🐛 Fixing a bug.完善 ConcurrentHashMap
2 parents 1ad8f22 + 9ded172 commit a6d3421

File tree

2 files changed

+50
-9
lines changed

2 files changed

+50
-9
lines changed

MD/ConcurrentHashMap.md

+46-5
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,24 @@
44

55
因此需要支持线程安全的并发容器 `ConcurrentHashMap`
66

7-
## 数据结构
7+
## JDK1.7 实现
8+
9+
### 数据结构
810
![](https://ws2.sinaimg.cn/large/006tNc79ly1fn2f5pgxinj30dw0730t7.jpg)
911

1012
如图所示,是由 `Segment` 数组、`HashEntry` 数组组成,和 `HashMap` 一样,仍然是数组加链表组成。
1113

1214
`ConcurrentHashMap` 采用了分段锁技术,其中 `Segment` 继承于 `ReentrantLock`。不会像 `HashTable` 那样不管是 `put` 还是 `get` 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 `CurrencyLevel` (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 `Segment` 时,不会影响到其他的 `Segment`
1315

14-
## get 方法
16+
### get 方法
1517
`ConcurrentHashMap``get` 方法是非常高效的,因为整个过程都不需要加锁。
1618

1719
只需要将 `Key` 通过 `Hash` 之后定位到具体的 `Segment` ,再通过一次 `Hash` 定位到具体的元素上。由于 `HashEntry` 中的 `value` 属性是用 `volatile` 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值([volatile 相关知识点](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Threadcore.md#%E5%8F%AF%E8%A7%81%E6%80%A7))。
1820

19-
## put 方法
21+
### put 方法
2022

2123
内部 `HashEntry` 类 :
24+
2225
```java
2326
static final class HashEntry<K,V> {
2427
final int hash;
@@ -41,7 +44,7 @@
4144

4245
而 ConcurrentHashMap 不一样,它是先将数据插入之后再检查是否需要扩容,之后再做插入。
4346

44-
## size 方法
47+
### size 方法
4548

4649
每个 `Segment` 都有一个 `volatile` 修饰的全局变量 `count` ,求整个 `ConcurrentHashMap``size` 时很明显就是将所有的 `count` 累加即可。但是 `volatile` 修饰的变量却不能保证多线程的原子性,所有直接累加很容易出现并发问题。
4750

@@ -50,4 +53,42 @@
5053
至于 `ConcurrentHashMap` 是如何知道在统计时大小发生了变化呢,每个 `Segment` 都有一个 `modCount` 变量,每当进行一次 `put remove` 等操作,`modCount` 将会 +1。只要 `modCount` 发生了变化就认为容器的大小也在发生变化。
5154

5255

53-
> 以上内容 base JDK1.7,1.8 的实现更加复杂但是原理类似,建议在 1.7 的基础上查看源码。
56+
57+
## JDK1.8 实现
58+
59+
![](https://ws3.sinaimg.cn/large/006tNc79gy1fthpv4odbsj30lp0drmxr.jpg)
60+
61+
1.8 中的 ConcurrentHashMap 数据结构和实现与 1.7 还是有着明显的差异。
62+
63+
其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性。
64+
65+
![](https://ws3.sinaimg.cn/large/006tNc79gy1fthq78e5gqj30nr09mmz9.jpg)
66+
67+
也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。
68+
69+
其中的 `val next` 都用了 volatile 修饰,保证了可见性。
70+
71+
### put 方法
72+
73+
重点来看看 put 函数:
74+
75+
![](https://ws3.sinaimg.cn/large/006tNc79gy1fthrz8jlo8j30oc0rbte3.jpg)
76+
77+
- 根据 key 计算出 hashcode 。
78+
- 判断是否需要进行初始化。
79+
- `f` 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
80+
- 如果当前位置的 `hashcode == MOVED == -1`,则需要进行扩容。
81+
- 如果都不满足,则利用 synchronized 锁写入数据。
82+
- 如果数量大于 `TREEIFY_THRESHOLD` 则要转换为红黑树。
83+
84+
### get 方法
85+
86+
![](https://ws1.sinaimg.cn/large/006tNc79gy1fthsnp2f35j30o409hwg7.jpg)
87+
88+
- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
89+
- 如果是红黑树那就按照树的方式获取值。
90+
- 就不满足那就按照链表的方式遍历获取值。
91+
92+
## 总结
93+
94+
1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(`O(logn)`),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

MD/Synchronize.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# Synchronize 关键字原理
1+
# synchronized 关键字原理
22

3-
众所周知 `Synchronize` 关键字是解决并发问题常用解决方案,有以下三种使用方式:
3+
众所周知 `synchronized` 关键字是解决并发问题常用解决方案,有以下三种使用方式:
44

55
- 同步普通方法,锁的是当前对象。
66
- 同步静态方法,锁的是当前 `Class` 对象。
7-
- 同步块,锁的是 `{}` 中的对象。
7+
- 同步块,锁的是 `()` 中的对象。
88

99

1010
实现原理:
@@ -71,7 +71,7 @@ public class com.crossoverjie.synchronize.Synchronize {
7171

7272

7373
## 锁优化
74-
`synchronize` 很多都称之为重量锁,`JDK1.6` 中对 `synchronize` 进行了各种优化,为了能减少获取和释放锁带来的消耗引入了`偏向锁``轻量锁`
74+
`synchronized` 很多都称之为重量锁,`JDK1.6` 中对 `synchronized` 进行了各种优化,为了能减少获取和释放锁带来的消耗引入了`偏向锁``轻量锁`
7575

7676

7777
### 轻量锁

0 commit comments

Comments
 (0)