mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-16 18:10:13 +08:00
[docs update]完善 ConcurrentHashMap 相关的问题
This commit is contained in:
parent
676e16802e
commit
36f59d4dbf
@ -65,11 +65,11 @@ head:
|
|||||||
|
|
||||||
如果你看过 `HashSet` 源码的话就应该知道:`HashSet` 底层就是基于 `HashMap` 实现的。(`HashSet` 的源码非常非常少,因为除了 `clone()`、`writeObject()`、`readObject()`是 `HashSet` 自己不得不实现之外,其他方法都是直接调用 `HashMap` 中的方法。
|
如果你看过 `HashSet` 源码的话就应该知道:`HashSet` 底层就是基于 `HashMap` 实现的。(`HashSet` 的源码非常非常少,因为除了 `clone()`、`writeObject()`、`readObject()`是 `HashSet` 自己不得不实现之外,其他方法都是直接调用 `HashMap` 中的方法。
|
||||||
|
|
||||||
| `HashMap` | `HashSet` |
|
| `HashMap` | `HashSet` |
|
||||||
| :------------------------------------: | :----------------------------------------------------------------------------------------------------------------------: |
|
| :------------------------------------: | :----------------------------------------------------------: |
|
||||||
| 实现了 `Map` 接口 | 实现 `Set` 接口 |
|
| 实现了 `Map` 接口 | 实现 `Set` 接口 |
|
||||||
| 存储键值对 | 仅存储对象 |
|
| 存储键值对 | 仅存储对象 |
|
||||||
| 调用 `put()`向 map 中添加元素 | 调用 `add()`方法向 `Set` 中添加元素 |
|
| 调用 `put()`向 map 中添加元素 | 调用 `add()`方法向 `Set` 中添加元素 |
|
||||||
| `HashMap` 使用键(Key)计算 `hashcode` | `HashSet` 使用成员对象来计算 `hashcode` 值,对于两个对象来说 `hashcode` 可能相同,所以`equals()`方法用来判断对象的相等性 |
|
| `HashMap` 使用键(Key)计算 `hashcode` | `HashSet` 使用成员对象来计算 `hashcode` 值,对于两个对象来说 `hashcode` 可能相同,所以`equals()`方法用来判断对象的相等性 |
|
||||||
|
|
||||||
### HashMap 和 TreeMap 区别
|
### HashMap 和 TreeMap 区别
|
||||||
@ -459,6 +459,83 @@ Java 8 中,锁粒度更细,`synchronized` 只锁定当前链表或红黑二
|
|||||||
- **Hash 碰撞解决方法** : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
|
- **Hash 碰撞解决方法** : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
|
||||||
- **并发度**:JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。
|
- **并发度**:JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。
|
||||||
|
|
||||||
|
### ConcurrentHashMap 为什么 key 和 value 不能为 null?
|
||||||
|
|
||||||
|
`ConcurrentHashMap` 的 key 和 value 不能为 null 主要是为了避免二义性。null 是一个特殊的值,表示没有对象或没有引用。如果你用 null 作为键,那么你就无法区分这个键是否存在于 `ConcurrentHashMap` 中,还是根本没有这个键。同样,如果你用 null 作为值,那么你就无法区分这个值是否是真正存储在 `ConcurrentHashMap` 中的,还是因为找不到对应的键而返回的。
|
||||||
|
|
||||||
|
拿 get 方法取值来说,返回的结果为 null 存在两种情况:
|
||||||
|
|
||||||
|
- 值没有在集合中 ;
|
||||||
|
- 值本身就是 null。
|
||||||
|
|
||||||
|
这也就是二义性的由来。
|
||||||
|
|
||||||
|
具体可以参考 [ConcurrentHashMap 源码分析](https://javaguide.cn/java/collection/concurrent-hash-map-source-code.html) 。
|
||||||
|
|
||||||
|
多线程环境下,存在一个线程操作该 `ConcurrentHashMap` 时,其他的线程将该 `ConcurrentHashMap` 修改的情况,所以无法通过 `containsKey(key)` 来判断否存在这个键值对,也就没办法解决二义性问题了。
|
||||||
|
|
||||||
|
与此形成对比的是,`HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个。如果传入 null 作为参数,就会返回 hash 值为 0 的位置的值。单线程环境下,不存在一个线程操作该 HashMap 时,其他的线程将该 `HashMap` 修改的情况,所以可以通过 `contains(key)`来做判断是否存在这个键值对,从而做相应的处理,也就不存在二义性问题。
|
||||||
|
|
||||||
|
也就是说,多线程下无法正确判定键值对是否存在(存在其他线程修改的情况),单线程是可以的(不存在其他线程修改的情况)。
|
||||||
|
|
||||||
|
如果你确实需要在 ConcurrentHashMap 中使用 null 的话,可以使用一个特殊的静态空对象来代替 null。
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static final Object NULL = new Object();
|
||||||
|
```
|
||||||
|
|
||||||
|
### ConcurrentHashMap 能保证复合操作的原子性吗?
|
||||||
|
|
||||||
|
`ConcurrentHashMap` 是线程安全的,意味着它可以保证多个线程同时对它进行读写操作时,不会出现数据不一致的情况。但是,这并不意味着它可以保证所有的复合操作都是原子性的。
|
||||||
|
|
||||||
|
复合操作是指由多个基本操作(如`put`、`get`、`remove`、`containsKey`等)组成的操作,例如先判断某个键是否存在`containsKey(key)`,然后根据结果进行插入或更新`put(key, value)`。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。
|
||||||
|
|
||||||
|
假设有两个线程 A 和 B 同时对 `ConcurrentHashMap` 进行复合操作,如下:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 线程 A
|
||||||
|
if (!map.containsKey(key)) {
|
||||||
|
map.put(key, value);
|
||||||
|
}
|
||||||
|
// 线程 B
|
||||||
|
if (!map.containsKey(key)) {
|
||||||
|
map.put(key, anotherValue);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
如果线程 A 和 B 的执行顺序是这样:
|
||||||
|
|
||||||
|
1. 线程 A 判断 map 中不存在 key
|
||||||
|
2. 线程 B 判断 map 中不存在 key
|
||||||
|
3. 线程 B 将 (key, anotherValue) 插入 map
|
||||||
|
4. 线程 A 将 (key, value) 插入 map
|
||||||
|
|
||||||
|
那么最终的结果是 (key, value),而不是预期的 (key, anotherValue)。这就是复合操作的非原子性导致的问题。
|
||||||
|
|
||||||
|
**那如何保证 `ConcurrentHashMap` 复合操作的原子性呢?**
|
||||||
|
|
||||||
|
`ConcurrentHashMap` 提供了一些原子性的复合操作,如 `putIfAbsent`、`compute`、`computeIfAbsent` 、`computeIfPresent`、`merge`等。这些方法都可以接受一个函数作为参数,根据给定的 key 和 value 来计算一个新的 value,并且将其更新到 map 中。
|
||||||
|
|
||||||
|
上面的代码可以改写为:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 线程 A
|
||||||
|
map.putIfAbsent(key, value);
|
||||||
|
// 线程 B
|
||||||
|
map.putIfAbsent(key, anotherValue);
|
||||||
|
```
|
||||||
|
|
||||||
|
或者:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 线程 A
|
||||||
|
map.computeIfAbsent(key, k -> value);
|
||||||
|
// 线程 B
|
||||||
|
map.computeIfAbsent(key, k -> anotherValue);
|
||||||
|
```
|
||||||
|
|
||||||
|
不建议使用加锁的同步机制,违背了使用 `ConcurrentHashMap` 的初衷。
|
||||||
|
|
||||||
## Collections 工具类(不重要)
|
## Collections 工具类(不重要)
|
||||||
|
|
||||||
**`Collections` 工具类常用方法**:
|
**`Collections` 工具类常用方法**:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user