|
4 | 4 |
|
5 | 5 | 因此需要支持线程安全的并发容器 `ConcurrentHashMap` 。
|
6 | 6 |
|
7 |
| -## 数据结构 |
| 7 | +## JDK1.7 实现 |
| 8 | + |
| 9 | +### 数据结构 |
8 | 10 | 
|
9 | 11 |
|
10 | 12 | 如图所示,是由 `Segment` 数组、`HashEntry` 数组组成,和 `HashMap` 一样,仍然是数组加链表组成。
|
11 | 13 |
|
12 | 14 | `ConcurrentHashMap` 采用了分段锁技术,其中 `Segment` 继承于 `ReentrantLock`。不会像 `HashTable` 那样不管是 `put` 还是 `get` 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 `CurrencyLevel` (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 `Segment` 时,不会影响到其他的 `Segment`。
|
13 | 15 |
|
14 |
| -## get 方法 |
| 16 | +### get 方法 |
15 | 17 | `ConcurrentHashMap` 的 `get` 方法是非常高效的,因为整个过程都不需要加锁。
|
16 | 18 |
|
17 | 19 | 只需要将 `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))。
|
18 | 20 |
|
19 |
| -## put 方法 |
| 21 | +### put 方法 |
20 | 22 |
|
21 | 23 | 内部 `HashEntry` 类 :
|
| 24 | + |
22 | 25 | ```java
|
23 | 26 | static final class HashEntry<K,V> {
|
24 | 27 | final int hash;
|
|
41 | 44 |
|
42 | 45 | 而 ConcurrentHashMap 不一样,它是先将数据插入之后再检查是否需要扩容,之后再做插入。
|
43 | 46 |
|
44 |
| -## size 方法 |
| 47 | +### size 方法 |
45 | 48 |
|
46 | 49 | 每个 `Segment` 都有一个 `volatile` 修饰的全局变量 `count` ,求整个 `ConcurrentHashMap` 的 `size` 时很明显就是将所有的 `count` 累加即可。但是 `volatile` 修饰的变量却不能保证多线程的原子性,所有直接累加很容易出现并发问题。
|
47 | 50 |
|
|
50 | 53 | 至于 `ConcurrentHashMap` 是如何知道在统计时大小发生了变化呢,每个 `Segment` 都有一个 `modCount` 变量,每当进行一次 `put remove` 等操作,`modCount` 将会 +1。只要 `modCount` 发生了变化就认为容器的大小也在发生变化。
|
51 | 54 |
|
52 | 55 |
|
53 |
| -> 以上内容 base JDK1.7,1.8 的实现更加复杂但是原理类似,建议在 1.7 的基础上查看源码。 |
| 56 | + |
| 57 | +## JDK1.8 实现 |
| 58 | + |
| 59 | + |
| 60 | + |
| 61 | +1.8 中的 ConcurrentHashMap 数据结构和实现与 1.7 还是有着明显的差异。 |
| 62 | + |
| 63 | +其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性。 |
| 64 | + |
| 65 | + |
| 66 | + |
| 67 | +也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。 |
| 68 | + |
| 69 | +其中的 `val next` 都用了 volatile 修饰,保证了可见性。 |
| 70 | + |
| 71 | +### put 方法 |
| 72 | + |
| 73 | +重点来看看 put 函数: |
| 74 | + |
| 75 | + |
| 76 | + |
| 77 | +- 根据 key 计算出 hashcode 。 |
| 78 | +- 判断是否需要进行初始化。 |
| 79 | +- `f` 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。 |
| 80 | +- 如果当前位置的 `hashcode == MOVED == -1`,则需要进行扩容。 |
| 81 | +- 如果都不满足,则利用 synchronized 锁写入数据。 |
| 82 | +- 如果数量大于 `TREEIFY_THRESHOLD` 则要转换为红黑树。 |
| 83 | + |
| 84 | +### get 方法 |
| 85 | + |
| 86 | + |
| 87 | + |
| 88 | +- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 |
| 89 | +- 如果是红黑树那就按照树的方式获取值。 |
| 90 | +- 就不满足那就按照链表的方式遍历获取值。 |
| 91 | + |
| 92 | +## 总结 |
| 93 | + |
| 94 | +1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(`O(logn)`),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。 |
0 commit comments