1. 锁结构
- JDK 1.7:
- 使用基于
Segment
的分段锁机制。Segment
是ReentrantLock
的子类,每个Segment
维护了一个HashEntry
数组,这些Segment
构成了一个Segment
数组。因此,ConcurrentHashMap
实际上是通过分段锁来实现的,每个Segment
都可以独立地加锁和解锁,从而提高了并发性能。 - 锁的粒度是段级别的,即每个
Segment
锁定的是一段哈希表结构。
- 使用基于
- JDK 1.8:
- 摒弃了
Segment
,转而使用synchronized
关键字结合CAS
(Compare-And-Swap)操作来实现线程安全。 - 锁的粒度缩小为结点锁,即只对实际发生冲突的节点进行加锁,从而进一步提高了并发性能。
- 摒弃了
2. 数据结构
- JDK 1.7:
- 使用
Segment
数组 +HashEntry
数组的结构。 - 每个
Segment
内部维护了一个HashEntry
数组,用于存储键值对。
- 使用
- JDK 1.8:
- 使用
Node
数组 + 链表 + 红黑树的结构。 - 当链表长度过长时(默认大于等于8),会转换为红黑树,以提高查询效率。
- 使用
3. put 方法的执行流程
- JDK 1.7:
- 需要进行两次定位:先对
Segment
进行定位,再对其内部的HashEntry
数组下标进行定位。 - 定位后,采用自旋锁 + 锁膨胀的机制进行加锁。如果自旋次数超过一定阈值(默认为64),则会发生锁膨胀,线程将陷入阻塞状态,等待唤醒。
- 在整个
put
操作期间都持有锁。
- 需要进行两次定位:先对
- JDK 1.8:
- 只需要一次定位,直接定位到
Node
数组的下标。 - 如果对应下标处没有节点,说明没有发生哈希冲突,此时直接通过
CAS
进行插入。 - 如果插入失败(即存在哈希冲突),则使用
synchronized
对该节点进行加锁,然后插入节点。
- 只需要一次定位,直接定位到
4. 计算 size 的方法
- JDK 1.7:
- 尝试在不加锁的情况下进行统计。如果前两次统计的结果相同,则直接返回该结果。
- 如果统计结果超过三次仍不一致,则对每个
Segment
进行加锁后再进行统计。
- JDK 1.8:
- 维护一个
baseCount
属性来记录节点数量。 - 每次
put
操作后,都会通过CAS
自增baseCount
。 - 由于并发环境下集合的大小是动态变化的,因此
size()
方法提供的是一个估计值,并不保证完全准确。
- 维护一个
-
总结
-
总结来说,JDK 1.8 相比于 JDK 1.7,主要区别在于:
- 结构上取消了
Segment
数组,改用数组 + 链表 + 红黑树结构。 - 线程安全机制由
Segment
的分段锁变为 CAS +synchronized
。 - 锁粒度由
Segment
级别变为单个Node
级别。 - 引入了红黑树来优化长链表的性能问题。
size
方法的实现不同,JDK 1.8 使用了更高效的统计方式