HashMap为什么不是线程安全的
在 JDK 1.7 中,HashMap 的底层存储结构是数组加链表,采用头插法插入数据。当多个线程并发进行 put 操作且发生扩容时,可能会出现环形链表,形成死循环。
在 JDK 1.8 中,HashMap 的底层存储结构是数组加链表加红黑树,采用了尾插法插入数据,解决了 JDK 1.7 中的环形链表问题,但仍存在数据丢失的情况。主要原因是 HashMap 本身不是线程安全的,当多个线程同时进行 put 操作时,可能会出现数据覆盖的情况。
例如,两个线程 A 和 B 同时进行 put 操作,且 hash 函数计算出的插入下标相同。线程 A 执行到判断是否出现 hash 碰撞的步骤后因时间片耗尽被挂起,线程 B 得到时间片后在该下标处插入元素并完成操作,线程 A 恢复后不再进行 hash 碰撞判断,直接插入数据,导致线程 B 插入的数据被覆盖。
ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
1.7
首先将数据分为一段一段(这个“段”就是 Segment
)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap
是由 Segment
数组结构和 HashEntry
数组结构组成。
static class Segment<K,V> extends ReentrantLock implements Serializable {
}
Segment
继承了 ReentrantLock
,所以 Segment
是一种可重入锁,扮演锁的角色。HashEntry
用于存储键值对数据。
一个 ConcurrentHashMap
里包含一个 Segment
数组,Segment
的个数一旦初始化就不能改变。 Segment
数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。
Segment
的结构和 HashMap
类似,是一种数组和链表结构,一个 Segment
包含一个 HashEntry
数组,每个 HashEntry
是一个链表结构的元素,每个 Segment
守护着一个 HashEntry
数组里的元素,当对 HashEntry
数组的数据进行修改时,必须首先获得对应的 Segment
的锁。也就是说,对同一 Segment
的并发写入会被阻塞,不同 Segment
的写入是可以并发执行的。
1.8
Java 8 几乎完全重写了 ConcurrentHashMap
,代码量从原来 Java 7 中的 1000 多行,变成了现在的 6000 多行。
ConcurrentHashMap
取消了 Segment
分段锁,采用 Node + CAS + synchronized
来保证并发安全。数据结构跟 HashMap
1.8 的结构类似,数组+链表/红黑二叉树。
确保高并发性能和数据结构的平衡性:
Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))
Java 8 中,锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。
参考链接: docs/java/collection/java-collection-questions-02.md · java小皮匠/JavaGuide - Gitee.com
源码分析
重点关注:构造方法,get(),put(),transfer()
ConcurrentHashMap 为什么 key 和 value 不能为 null?
ConcurrentHashMap
的 key 和 value 不能为 null 主要是为了避免二义性。null 是一个特殊的值,表示没有对象或没有引用。如果你用 null 作为键,那么你就无法区分这个键是否存在于 ConcurrentHashMap
中,还是根本没有这个键。同样,如果你用 null 作为值,那么你就无法区分这个值是否是真正存储在 ConcurrentHashMap
中的,还是因为找不到对应的键而返回的。
拿 get 方法取值来说,返回的结果为 null 存在两种情况:
- 值没有在集合中 ;
- 值本身就是 null。
这也就是二义性的由来。
与此形成对比的是,HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个。如果传入 null 作为参数,就会返回 hash 值为 0 的位置的值。单线程环境下,不存在一个线程操作该 HashMap 时,其他的线程将该 HashMap
修改的情况,所以可以通过 contains(key)
来做判断是否存在这个键值对,从而做相应的处理,也就不存在二义性问题。
ConcurrentHashMap 能保证复合操作的原子性吗?
不能
复合操作是指由多个基本操作(如put
、get
、remove
、containsKey
等)组成的操作,例如先判断某个键是否存在containsKey(key)
,然后根据结果进行插入或更新put(key, value)
。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。
ConcurrentHashMap
提供了一些原子性的复合操作,如 putIfAbsent
、compute
、computeIfAbsent
、computeIfPresent
、merge
等。这些方法都可以接受一个函数作为参数,根据给定的 key 和 value 来计算一个新的 value,并且将其更新到 map 。