Skip to content

Commit 0980514

Browse files
authored
Merge pull request #2527 from HoeYeungHo/main
更新 `ConcurrentHashMap` 1.7 的扩容机制和集合判空中的例子
2 parents 388f94d + 3453b2f commit 0980514

File tree

2 files changed

+50
-15
lines changed

2 files changed

+50
-15
lines changed

docs/java/collection/concurrent-hash-map-source-code.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,19 @@ private void rehash(HashEntry<K,V> node) {
368368
}
369369
```
370370

371-
有些同学可能会对最后的两个 for 循环有疑惑,这里第一个 for 是为了寻找这样一个节点,这个节点后面的所有 next 节点的新位置都是相同的。然后把这个作为一个链表赋值到新位置。第二个 for 循环是为了把剩余的元素通过头插法插入到指定位置链表。这样实现的原因可能是基于概率统计,有深入研究的同学可以发表下意见。
371+
有些同学可能会对最后的两个 for 循环有疑惑,这里第一个 for 是为了寻找这样一个节点,这个节点后面的所有 next 节点的新位置都是相同的。然后把这个作为一个链表赋值到新位置。第二个 for 循环是为了把剩余的元素通过头插法插入到指定位置链表。~~这样实现的原因可能是基于概率统计,有深入研究的同学可以发表下意见。~~
372+
373+
内部第二个 `for` 循环中使用了 `new HashEntry<K,V>(h, p.key, v, n)` 创建了一个新的 `HashEntry`,而不是复用之前的,是因为如果复用之前的,那么会导致正在遍历(如正在执行 `get` 方法)的线程由于指针的修改无法遍历下去。正如注释中所说的:
374+
375+
> 当它们不再被可能正在并发遍历表的任何读取线程引用时,被替换的节点将被垃圾回收。
376+
>
377+
> The nodes they replace will be garbage collectable as soon as they are no longer referenced by any reader thread that may be in the midst of concurrently traversing table
378+
379+
为什么需要再使用一个 `for` 循环找到 `lastRun` ,其实是为了减少对象创建的次数,正如注解中所说的:
380+
381+
> 从统计上看,在默认的阈值下,当表容量加倍时,只有大约六分之一的节点需要被克隆。
382+
>
383+
> Statistically, at the default threshold, only about one-sixth of them need cloning when a table doubles.
372384
373385
### 5. get
374386

docs/java/collection/java-collection-precautions-for-use.md

+37-14
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,58 @@ tag:
1515

1616
> **判断所有集合内部的元素是否为空,使用 `isEmpty()` 方法,而不是 `size()==0` 的方式。**
1717
18-
这是因为 `isEmpty()` 方法的可读性更好,并且时间复杂度为 O(1)。
18+
这是因为 `isEmpty()` 方法的可读性更好,并且时间复杂度为 `O(1)`
1919

20-
绝大部分我们使用的集合的 `size()` 方法的时间复杂度也是 O(1),不过,也有很多复杂度不是 O(1) 的,比如 `java.util.concurrent` 包下的某些集合(`ConcurrentLinkedQueue``ConcurrentHashMap`...)。
20+
绝大部分我们使用的集合的 `size()` 方法的时间复杂度也是 `O(1)`,不过,也有很多复杂度不是 `O(1)` 的,比如 `java.util.concurrent` 包下的 `ConcurrentLinkedQueue``ConcurrentLinkedQueue``isEmpty()` 方法通过 `first()` 方法进行判断,其中 `first()` 方法返回的是队列中第一个值不为 `null` 的节点(节点值为`null`的原因是在迭代器中使用的逻辑删除)
2121

22-
下面是 `ConcurrentHashMap``size()` 方法和 `isEmpty()` 方法的源码。
22+
```java
23+
public boolean isEmpty() { return first() == null; }
24+
25+
Node<E> first() {
26+
restartFromHead:
27+
for (;;) {
28+
for (Node<E> h = head, p = h, q;;) {
29+
boolean hasItem = (p.item != null);
30+
if (hasItem || (q = p.next) == null) { // 当前节点值不为空 或 到达队尾
31+
updateHead(h, p); // 将head设置为p
32+
return hasItem ? p : null;
33+
}
34+
else if (p == q) continue restartFromHead;
35+
else p = q; // p = p.next
36+
}
37+
}
38+
}
39+
```
40+
41+
由于在插入与删除元素时,都会执行`updateHead(h, p)`方法,所以该方法的执行的时间复杂度可以近似为`O(1)`。而 `size()` 方法需要遍历整个链表,时间复杂度为`O(n)`
2342

2443
```java
2544
public int size() {
26-
long n = sumCount();
27-
return ((n < 0L) ? 0 :
28-
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
29-
(int)n);
45+
int count = 0;
46+
for (Node<E> p = first(); p != null; p = succ(p))
47+
if (p.item != null)
48+
if (++count == Integer.MAX_VALUE)
49+
break;
50+
return count;
3051
}
52+
```
53+
54+
此外,在`ConcurrentHashMap` 1.7 中 `size()` 方法和 `isEmpty()` 方法的时间复杂度也不太一样。`ConcurrentHashMap` 1.7 将元素数量存储在每个`Segment` 中,`size()` 方法需要统计每个 `Segment` 的数量,而 `isEmpty()` 只需要找到第一个不为空的 `Segment` 即可。但是在`ConcurrentHashMap` 1.8 中的 `size()` 方法和 `isEmpty()` 都需要调用 `sumCount()` 方法,其时间复杂度与 `Node` 数组的大小有关。下面是 `sumCount()` 方法的源码:
55+
56+
```java
3157
final long sumCount() {
3258
CounterCell[] as = counterCells; CounterCell a;
3359
long sum = baseCount;
34-
if (as != null) {
35-
for (int i = 0; i < as.length; ++i) {
60+
if (as != null)
61+
for (int i = 0; i < as.length; ++i)
3662
if ((a = as[i]) != null)
3763
sum += a.value;
38-
}
39-
}
4064
return sum;
4165
}
42-
public boolean isEmpty() {
43-
return sumCount() <= 0L; // ignore transient negative values
44-
}
4566
```
4667

68+
这是因为在并发的环境下,`ConcurrentHashMap` 将每个 `Node` 中节点的数量存储在 `CounterCell[]` 数组中。在 `ConcurrentHashMap` 1.7 中,将元素数量存储在每个`Segment` 中,`size()` 方法需要统计每个 `Segment` 的数量,而 `isEmpty()` 只需要找到第一个不为空的 `Segment` 即可。
69+
4770
## 集合转 Map
4871

4972
《阿里巴巴 Java 开发手册》的描述如下:

0 commit comments

Comments
 (0)