java 7 分段锁
在 Java 7 及更早的版本中,ConcurrentHashMap 采用了分段锁的策略:
- 分段(Segment) :整个哈希表被划分为多个独立的段(通常是 16 个),每个段维护自己的一部分数据。
- 锁粒度:每个段有一个独立的锁(
ReentrantLock),当一个线程要修改某个段的数据时,只需锁定对应的段,而不是整个哈希表。 - 并发性:由于多个段可以并行地被不同线程锁定和修改,整体并发性能得到了提升。
优点:
- 减少了锁的竞争,提高了并发度。
缺点:
- 锁的粒度固定,随着线程数增加,可能会导致部分段成为热点,限制了扩展性。
Jdk 1.8 CAS
从 JDK 1.8 开始,ConcurrentHashMap 摒弃了分段锁的设计,改用了更细粒度的控制机制,比如 CAS 操作,来减少锁的开销。
- 在 JDK 1.8 中,ConcurrentHashMap 使用了
CAS操作来实现部分无锁化的更新操作。CAS 是一种硬件级别的原子操作,用于实现无锁的线程安全操作。 - CAS 主要用于在插入和更新节点时,以无锁的方式确保节点插入的原子性。例如,当某个桶位为空时,使用 CAS 操作将新节点放入该位置。
链表/红黑树结构
在 Java 8 中,ConcurrentHashMap 使用了链表与红黑树相结合的结构:当链表中的元素超过一定阈值时,会自动转换为红黑树,这样查找和插入的性能在冲突较多时依然可以保持在 O(logN) 的水平。
扩容过程中的 synchronized 锁定机制
扩容的关键步骤包括:
- 创建一个新的、更大的桶数组。
- 将旧桶中的数据重新哈希并分布到新的桶中。
- 锁定桶 1 的头节点 (Node3)
- 重新计算桶 1 中的元素的哈希值,并将其分配到新的桶中
- 释放桶 1 的锁
- 旧桶的数据已被安全迁移到新桶
在扩容过程中,如果线程 A 发现某个桶需要重新分配,它会尝试锁定该桶的头节点。由于只有头节点被锁定,因此其他线程仍然可以访问其他桶的数据,保持了一定的并发性。
桶内重排序 synchronized 锁定
当桶内的链表长度超过阈值(例如 8)时,ConcurrentHashMap 会将链表转化为红黑树。这也是一种需要 synchronized 锁定头节点的操作,因为在链表转化为红黑树的过程中,不希望其他线程同时对该桶的数据进行修改。
举例说明
[一个大的数组],数组里每个元素进行put操作,都是有一个不同的锁,刚开始进行put的时候,如果两个线程都是在数组[5]这个位置进行put,这个时候,对数组[5]这个位置进行put的时候,采取的是CAS的策略
同一个时间,只有一个线程能成功执行这个CAS,就是说他刚开始先获取一下数组[5]这个位置的值,null,然后执行CAS,线程1,比较一下,put进去我的这条数据,同时间,其他的线程执行CAS,都会失败
分段加锁,通过对数组每个元素执行CAS的策略,如果是很多线程对数组里不同的元素执行put,大家是没有关系的,如果其他人失败了,其他人此时会发现说,数组[5]这位置,已经给刚才又人放进去值了
就需要在这个位置基于链表+红黑树来进行处理,synchronized(数组[5]),加锁,基于链表或者是红黑树在这个位置插进去自己的数据
如果你是对数组里同一个位置的元素进行操作,才会加锁串行化处理;如果是对数组不同位置的元素操作,此时大家可以并发执行的