一、先说结论
在JDK1.7版本中,ConcurrentHashMap使用了Segment分段锁机制,每个Segment相当于一个小的HashMap,它们各自判读和执行扩容,当需要扩容时,生成一个新的数组,将元素重新分配到新的数组中。
到了JDK1.8版本中,Segment机制被取消了,直接使用了细颗粒度的锁机制,当需要扩容时,如果已有线程在扩容,其它的线程会参与扩容,提升效率,扩容时,生成新数组并将元素分组,多个线程并行完成元素的迁移工作。这样提高了并发性能和扩容速度。
举个形象的例子:
JDK 1.7版本的ConcurrentHashMap扩容机制——分段锁机制
想象有一个大型仓库(
ConcurrentHashMap
),这个仓库被分成了多个房间(Segment
),每个房间可以独立存放物品(键值对)。每个房间都有自己的锁(锁
),只有持有钥匙(锁)的员工才能进入房间进行操作。当某个房间的存储空间快满了(达到阈值),房间负责人就决定扩建房间。扩建时,会将房间中的物品搬到一个更大的房间里。其他房间的员工仍然可以继续工作(因为他们不受这个房间扩建的影响),这就相当于每个房间(
Segment
)独立进行扩容。总结:在JDK 1.7版本中,每个房间(
Segment
)都像是一个独立的小型仓库,独立进行扩容操作。这样可以让多个房间同时进行操作而互不干扰,从而提高整体效率。JDK 1.8版本的ConcurrentHashMap扩容机制——细粒度锁控制
在JDK 1.8中,想象有一个没有分割的大型仓库(整个
ConcurrentHashMap
),仓库里的物品都放在同一个空间里。如果仓库需要扩建(扩容),就需要将所有物品搬到一个新的、更大的仓库中。当某个员工发现仓库空间不够,需要扩建时,他会开始搬运物品(触发扩容)。如果其他员工此时也想添加物品(put操作),他们会发现仓库正在扩建,于是他们会加入搬运队伍,一起将物品搬到新仓库里。
搬运物品时,大家会先把物品分组(分桶),然后每个员工负责一组物品的搬运工作。这样不仅加快了搬运速度,还减少了搬运时间内仓库的拥堵(线程竞争)。
总结:在JDK 1.8版本中,仓库不再分房间,而是一个整体。扩容时,如果有多个员工(线程)需要操作,他们可以一起参与扩建工作,从而提高扩建效率,减少了单一扩建操作的瓶颈问题。
二、ConcurrentHashMap
ConcurrentHashMap
是Java中的线程安全的哈希表实现,设计用于在并发环境中高效地处理键值对的存储和访问操作。它最显著的特点是通过多线程并发控制机制来提高操作效率,同时确保线程安全。
1. 基本概念
ConcurrentHashMap
继承自AbstractMap
类,并实现了ConcurrentMap
接口。它与HashMap
类似,都是键值对的集合,但ConcurrentHashMap
专为多线程环境设计,可以在没有全局锁的情况下进行并发读写。
2. 线程安全机制
ConcurrentHashMap
通过以下机制实现线程安全:
-
分段锁机制(JDK 1.7之前):在JDK 1.7及之前的版本中,
ConcurrentHashMap
采用了分段锁(Segment)的设计。整个哈希表被分成多个Segment
,每个Segment
类似于一个独立的小型HashMap
。这种设计允许多个线程同时访问不同的Segment
,从而提高并发性能。每个Segment
有自己独立的锁,因此在操作不同的Segment
时,不会相互阻塞。 -
细粒度锁控制(JDK 1.8及以后):从JDK 1.8开始,
ConcurrentHashMap
不再使用分段锁设计,而是采用了一种更加细粒度的锁控制,结合了CAS(Compare-And-Swap)操作、synchronized
块和Node
级别的锁来保证并发安全。这样可以让多个线程在同一时间操作同一个哈希表中的不同部分,提高了并发度。
3. 主要特点
-
高效的并发读写:
ConcurrentHashMap
允许多线程并发地进行读操作,读操作不需要加锁。对于写操作(如put
、remove
等),使用细粒度的锁或CAS机制来保证线程安全,避免全表加锁带来的性能瓶颈。 -
非阻塞读取:在绝大多数情况下,读取操作不会被锁定,这意味着多个线程可以同时读取数据,极大地提高了并发访问的效率。
-
分段锁定机制(JDK 1.7):通过分段锁,
ConcurrentHashMap
允许多个线程同时操作不同的段(Segment),进一步减少了锁竞争。 -
动态扩容:当
ConcurrentHashMap
中的元素达到一定数量时,会自动进行扩容。扩容过程会生成一个更大的数组,将原有数据重新分配到新数组中。JDK 1.8的实现允许多个线程共同参与扩容,提升扩容效率。 -
不允许null键或值:与
HashMap
不同,ConcurrentHashMap
不允许使用null
作为键或值,这样设计是为了避免多线程环境下的歧义问题。
4. 使用场景
ConcurrentHashMap
适用于需要频繁进行并发读写操作的场景,如缓存实现、频繁的并发查询和更新等。它可以在高并发情况下提供比Collections.synchronizedMap(new HashMap<>())
更好的性能。