@@ -65,11 +65,11 @@ head:
65
65
66
66
如果你看过 ` HashSet ` 源码的话就应该知道:` HashSet ` 底层就是基于 ` HashMap ` 实现的。(` HashSet ` 的源码非常非常少,因为除了 ` clone() ` 、` writeObject() ` 、` readObject() ` 是 ` HashSet ` 自己不得不实现之外,其他方法都是直接调用 ` HashMap ` 中的方法。
67
67
68
- | ` HashMap ` | ` HashSet ` |
69
- | :------------------------------------: | :---------------------------------------------------------------------------------------------------------------------- : |
70
- | 实现了 ` Map ` 接口 | 实现 ` Set ` 接口 |
71
- | 存储键值对 | 仅存储对象 |
72
- | 调用 ` put() ` 向 map 中添加元素 | 调用 ` add() ` 方法向 ` Set ` 中添加元素 |
68
+ | ` HashMap ` | ` HashSet ` |
69
+ | :------------------------------------: | :----------------------------------------------------------: |
70
+ | 实现了 ` Map ` 接口 | 实现 ` Set ` 接口 |
71
+ | 存储键值对 | 仅存储对象 |
72
+ | 调用 ` put() ` 向 map 中添加元素 | 调用 ` add() ` 方法向 ` Set ` 中添加元素 |
73
73
| ` HashMap ` 使用键(Key)计算 ` hashcode ` | ` HashSet ` 使用成员对象来计算 ` hashcode ` 值,对于两个对象来说 ` hashcode ` 可能相同,所以` equals() ` 方法用来判断对象的相等性 |
74
74
75
75
### HashMap 和 TreeMap 区别
@@ -459,6 +459,83 @@ Java 8 中,锁粒度更细,`synchronized` 只锁定当前链表或红黑二
459
459
- ** Hash 碰撞解决方法** : JDK 1.7 采用拉链法,JDK1 . 8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
460
460
- ** 并发度** :JDK 1.7 最大并发度是 Segment 的个数,默认是 16 。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。
461
461
462
+ ### ConcurrentHashMap 为什么 key 和 value 不能为 null ?
463
+
464
+ `ConcurrentHashMap ` 的 key 和 value 不能为 null 主要是为了避免二义性。null 是一个特殊的值,表示没有对象或没有引用。如果你用 null 作为键,那么你就无法区分这个键是否存在于 `ConcurrentHashMap ` 中,还是根本没有这个键。同样,如果你用 null 作为值,那么你就无法区分这个值是否是真正存储在 `ConcurrentHashMap ` 中的,还是因为找不到对应的键而返回的。
465
+
466
+ 拿 get 方法取值来说,返回的结果为 null 存在两种情况:
467
+
468
+ - 值没有在集合中 ;
469
+ - 值本身就是 null 。
470
+
471
+ 这也就是二义性的由来。
472
+
473
+ 具体可以参考 [ConcurrentHashMap 源码分析](https: // javaguide.cn/java/collection/concurrent-hash-map-source-code.html) 。
474
+
475
+ 多线程环境下,存在一个线程操作该 `ConcurrentHashMap ` 时,其他的线程将该 `ConcurrentHashMap ` 修改的情况,所以无法通过 `containsKey(key)` 来判断否存在这个键值对,也就没办法解决二义性问题了。
476
+
477
+ 与此形成对比的是,`HashMap ` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个。如果传入 null 作为参数,就会返回 hash 值为 0 的位置的值。单线程环境下,不存在一个线程操作该 HashMap 时,其他的线程将该 `HashMap ` 修改的情况,所以可以通过 `contains(key)`来做判断是否存在这个键值对,从而做相应的处理,也就不存在二义性问题。
478
+
479
+ 也就是说,多线程下无法正确判定键值对是否存在(存在其他线程修改的情况),单线程是可以的(不存在其他线程修改的情况)。
480
+
481
+ 如果你确实需要在 ConcurrentHashMap 中使用 null 的话,可以使用一个特殊的静态空对象来代替 null 。
482
+
483
+ ```java
484
+ public static final Object NULL = new Object ();
485
+ ```
486
+
487
+ ### ConcurrentHashMap 能保证复合操作的原子性吗?
488
+
489
+ `ConcurrentHashMap ` 是线程安全的,意味着它可以保证多个线程同时对它进行读写操作时,不会出现数据不一致的情况。但是,这并不意味着它可以保证所有的复合操作都是原子性的。
490
+
491
+ 复合操作是指由多个基本操作(如`put`、`get`、`remove`、`containsKey`等)组成的操作,例如先判断某个键是否存在`containsKey(key)`,然后根据结果进行插入或更新`put(key, value)`。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。
492
+
493
+ 假设有两个线程 A 和 B 同时对 `ConcurrentHashMap ` 进行复合操作,如下:
494
+
495
+ ```java
496
+ // 线程 A
497
+ if (! map. containsKey(key)) {
498
+ map. put(key, value);
499
+ }
500
+ // 线程 B
501
+ if (! map. containsKey(key)) {
502
+ map. put(key, anotherValue);
503
+ }
504
+ ```
505
+
506
+ 如果线程 A 和 B 的执行顺序是这样:
507
+
508
+ 1. 线程 A 判断 map 中不存在 key
509
+ 2. 线程 B 判断 map 中不存在 key
510
+ 3. 线程 B 将 (key, anotherValue) 插入 map
511
+ 4. 线程 A 将 (key, value) 插入 map
512
+
513
+ 那么最终的结果是 (key, value),而不是预期的 (key, anotherValue)。这就是复合操作的非原子性导致的问题。
514
+
515
+ ** 那如何保证 `ConcurrentHashMap ` 复合操作的原子性呢?**
516
+
517
+ `ConcurrentHashMap ` 提供了一些原子性的复合操作,如 `putIfAbsent`、`compute`、`computeIfAbsent` 、`computeIfPresent`、`merge`等。这些方法都可以接受一个函数作为参数,根据给定的 key 和 value 来计算一个新的 value,并且将其更新到 map 中。
518
+
519
+ 上面的代码可以改写为:
520
+
521
+ ```java
522
+ // 线程 A
523
+ map. putIfAbsent(key, value);
524
+ // 线程 B
525
+ map. putIfAbsent(key, anotherValue);
526
+ ```
527
+
528
+ 或者:
529
+
530
+ ```java
531
+ // 线程 A
532
+ map. computeIfAbsent(key, k - > value);
533
+ // 线程 B
534
+ map. computeIfAbsent(key, k - > anotherValue);
535
+ ```
536
+
537
+ 不建议使用加锁的同步机制,违背了使用 `ConcurrentHashMap ` 的初衷。
538
+
462
539
## Collections 工具类(不重要)
463
540
464
541
** `Collections ` 工具类常用方法** :
0 commit comments