ConcurrentHashMap

介绍

  • 在JDK7下,采用的分段锁的方式来保证数据安全,为每段数据分配一个锁,同时其他段的数据可以被其他线程访问。ConcurrentHashMap 由segment + HashEntry 组成,segment 内部实现了 ReetrantLock,一个 segment 守护一个 HashEntry,所以当几个线程竞争 HashEntry时,会用对应的ReentrantLock进行加锁。

  • 在JDK8下, ConcurrentHashMap 数据结构同 jdk8 中的 HashMap 数据结构一样,都是 数组+链表+红黑树。摒弃了 jdk7 中的分段锁设计,使用了 Node + CAS + Synchronized 来保证线程安全。

put流程

  • 详细版:

    • 在 JDK 7 中,ConcurrentHashMap 内部使用了分段锁(Segment)的机制来实现并发安全。每个 Segment 继承了 ReentrantLock,通过对每个 Segment 进行加锁,实现了对元素的并发插入操作的保护。具体的 put 操作流程如下:

      1. 根据键的哈希值确定要插入的 Segment。ConcurrentHashMap 内部维护了多个 Segment,每个 Segment 维护了一部分键值对。

      1. 获取对应的 Segment 的锁。通过调用 Segment 的 lock 方法,获取对应 Segment 的锁,保证只有一个线程可以对该 Segment 进行写操作。

      1. 在该 Segment 中进行插入操作。首先,根据键的哈希值计算出在 Segment 的内部数组中的槽位索引。然后,遍历该槽位的链表或红黑树,查找是否已经存在相同的键。如果存在相同的键,则更新对应的值;如果不存在相同的键,则创建新的节点,并将节点插入到链表或红黑树中。

      1. 释放 Segment 的锁。在完成插入操作后,释放对应 Segment 的锁,让其他线程可以继续对该 Segment 进行操作。

    • 在 JDK 8 中,ConcurrentHashMap 进行了一些改进。对于数组槽位为空的情况,采用了 CAS 和自旋的方式进行存入操作,避免了加锁的开销。具体的 put 操作流程如下:

      1. 根据键的哈希值确定要插入的数组槽位。ConcurrentHashMap 内部维护了一个 Node 数组,每个槽位可以存放一个或多个节点。

      2. 判断数组槽位是否为空。如果槽位为空,使用 CAS 操作尝试将新的节点插入到槽位中。通过 CAS 操作可以确保只有一个线程能够成功插入节点,其他线程需要自旋等待。

      3. 如果槽位不为空,即存在哈希冲突,则需要对槽位中的链表或红黑树进行操作。此时,使用 synchronized 关键字对链表或红黑树的头节点进行加锁,保证对链表或红黑树的操作是线程安全的。

      4. 在链表或红黑树中进行插入操作。类似于 JDK 7,根据键的哈希值在链表或红黑树中查找是否已经存在相同的键。如果存在相同的键,则更新对应的值;如果不存在相同的键,则创建新的节点,并将节点插入到链表或红黑树中。

  • 简化版:

    • jdk7下,基于segment的,内部继承了ReentranLock,每次put加锁,保证元素存入。

    • jdk8下,计算hash值,发现对应的Node数组槽位为空时,采用CAS+自旋保证存入,只有出现hash冲突,才会采用synchronized对Node加锁保证 链表/红黑树节点 存入。这样可以提高并发性能,减少锁的开销。

size方法

  • ConcurrentHashMap的put方法中不能进行加锁统计元素,这样会影响put的效率

  • ConcurrentHashMap的size方法是通过 baseCount + CountCell 数组 的方式进行统计

    • 多个线程首先会通过CAS进行baseCount累加,如果其中一个线程累加失败。会将其热点值的计算打散到CountCell数组的各个槽位,每个线程对应一个槽位进行累加即可,最终结果 = baseCount + CountCell数组各个槽位值

  • 站在size计算的角度上是线程安全的,但是从全局的角度下并不是,因为put方法和size方法之间的数据可能并不是一致的。

    • 举例:在并发环境下,size和put并发执行,size先执行完,put后执行结束,那size和put方法的数据就不一致了

原子性操作

  • ConcurrentHashMap 的原子性操作是通过 CAS操作 + volatile + 分段设计和锁分段等底层机制实现的。

    • CAS(Compare and Swap)操作是一种无锁的原子操作,用于解决并发环境下的数据竞争问题。它比较内存地址中的值与期望值,如果相等,则替换为新值。ConcurrentHashMap 在插入、更新和删除元素时使用 CAS 操作来保证原子性。

    • Volatile 关键字用于确保对内存的可见性。当一个线程修改共享变量的值时,其他线程可以立即看到这个修改,避免使用过期的缓存值。

    • ConcurrentHashMap 使用分段设计和锁分段来提高并发性能。它将整个哈希表分成多个段,每个段维护一个独立的哈希表,并拥有自己的锁。分段设计可以 减小并发冲突的范围,提高并发性能

通过以上底层机制的组合,ConcurrentHashMap 实现了线程安全的原子性操作。CAS 操作保证了插入、更新和删除操作的原子性,volatile 关键字确保了内存的可见性,分段设计和锁分段提供了并发控制和线程安全。

并发控制

  • ConcurrentHashMap 的并发控制依然是通过 CAS操作 + volatile + 分段设计和锁分段 来实现的。

    • ConcurrentHashMap 在实现并发控制时采用了分段锁(Segment Locking)的策略。它将整个哈希表分成一定数量的段(Segments),每个段维护一个独立的哈希表,并且拥有自己的锁(ReentrantLock)。

    • 这种分段锁的设计使得多个线程可以同时访问不同的段,从而提高并发性能。每个段内部的操作是串行的,即同一段内的操作会获取该段对应的锁,确保同一时刻只有一个线程可以修改该段的数据。

    • 当进行插入、删除或查询操作时,ConcurrentHashMap 首先根据键的哈希值定位到对应的段,然后在该段上获取锁,对该段的数据进行操作。这样,在多线程环境下,不同线程可以同时对不同的段进行操作,从而提高并发性能。

    • 另外,ConcurrentHashMap 在内部的段数组中使用了 volatile 关键字来保证对数组的可见性。这样,当一个线程修改了数组中的某个段时,其他线程可以立即看到这个修改,避免使用过期的缓存值。

    • 通过分段锁和 volatile 关键字的组合,ConcurrentHashMap 实现了高效的并发控制,保证了线程安全和数据一致性。

扩容机制

  • ConcurrentHashMap的扩容实质上也是数组的扩容,会重新开辟一份原数组容量2倍的数组并将老数组的元素移过来。

    • 在JDK7下基于分段锁实现的,每个segment都维护了一个HashEntry就是一个小的HashMap,所以每个segment都会进行扩容,操作和HashMap扩容一样。

    • 在JDK8下倒序遍历Node数组的槽位,每经过一个槽位就通过synchronized对Node节点进行加锁,对于不同的槽位类型(链表/红黑树)执行拷贝方法,对于红黑树会通过高位和低位(依旧是老位置的元素和新位置的元素)拆分成两个子链表存放到不同的槽位。

名词

  • Node:

    • 保存 key,value 及 key 的 hash 值的数据结构。其中 value 和 next 都用 volatile 修饰,保证并发的可见性。同时也可以禁止指令重排序

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值