ConcurrentHashMap 实现原理
ConcurrentHashMap是Java集合框架中的一个线程安全的哈希表实现,用于支持高并发的读写操作。
JDK 1.7中的ConcurrentHashMap实现原理
-
分段锁机制:
-
ConcurrentHashMap内部维护一个分段数组(Segment Array),该数组包含一定数量的分段(Segment)。每个分段实际上是一个独立的哈希表,分段数组的大小通常是默认的16。这意味着理论上最多可以同时支持16个线程并发写,只要它们的操作分别分布在不同的Segment上。
-
每个分段都有一个独立的锁(ReentrantLock的实例),用于控制该分段内的并发操作。这种分段锁机制允许多个线程同时访问和修改不同的分段,提高了并发性能。
-
-
哈希函数:
- 在ConcurrentHashMap中,元素的插入、获取和删除等操作都会使用哈希函数来确定元素应该存储在哪个分段中。哈希函数会根据元素的键来计算一个哈希码,然后将哈希码与分段数量取模,以确定应该存放在哪个分段中。
-
操作并发:
- 当执行插入、获取或删除操作时,首先使用哈希函数确定应该在哪个分段中执行操作。然后,锁住该分段的锁,以确保只有一个线程可以在该分段内执行操作。其他线程可以在其他分段内并发执行操作。
-
扩容机制:
- 当ConcurrentHashMap中的某一个分段需要扩容时,它只会锁住该分段,而不会影响其他分段的操作。扩容是在必要时进行的,而不是一开始就为整个哈希表分配足够大的空间。需要注意的是,Segment分段数组不能扩容,扩容的是segment分段数组内部的HashEntry<K,V>[]数组,扩容后容量为原来的2倍,并且会进行重散列(rehashing)操作。
JDK 1.8中的ConcurrentHashMap实现原理
-
数据结构:
- JDK 1.8中的ConcurrentHashMap摒弃了Segment的概念,直接采用数组 + 链表或红黑树的组合方式。引入红黑树结构,当节点数超过指定值时(默认为8),自动将链表转换成红黑树。这是因为链表查询的时间复杂度为
O(n)
,而红黑树查询的时间复杂度为O(log(n))
,节点较多时,可大大提升性能。反之,当节点数减少到一定程度时(默认小于6),红黑树会转换为链表。
- JDK 1.8中的ConcurrentHashMap摒弃了Segment的概念,直接采用数组 + 链表或红黑树的组合方式。引入红黑树结构,当节点数超过指定值时(默认为8),自动将链表转换成红黑树。这是因为链表查询的时间复杂度为
-
并发控制:
- 采用synchronized + CAS的方式。锁粒度细化到单个Bucket的head节点,不影响其他Bucket节点的读写,粒度更细,效率更高。查找、替换、赋值等操作使用CAS方式加锁,效率高。扩容时,使用synchronized锁住整个哈希表,阻塞所有的读写操作。
-
put操作:
-
如果没有初始化就先调用
initTable()
方法来进行初始化。 -
如果没有hash冲突就直接CAS插入。
-
如果还在进行扩容操作就先进行扩容。
-
如果存在hash冲突,就加锁来保证线程安全。这里有两种情况:一种是链表形式就直接遍历到尾端插入;一种是红黑树就按照红黑树格式插入。
-
如果该链表的数量大于阈值8,就要先转换成红黑树结构,然后再插入。
-
如果添加成功就调用
addCount()
方法统计size,并且检查是否需要扩容。
-
-
get操作:
- get操作可以无锁进行,因为Node的val和next使用volatile修饰,保证了内存可见性。
-
扩容机制:
- 当某个线程put时,如果发现ConcurrentHashMap正在进行扩容,那么该线程会一起进行扩容。ConcurrentHashMap支持多个线程同时扩容,扩容之前会新生成一个新的数组,然后将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作。
JDK 1.7与JDK 1.8实现原理的比较
-
锁机制:JDK 1.7使用分段锁(Segment Lock),而JDK 1.8使用synchronized和CAS操作。JDK 1.8的锁粒度更细,效率更高。
-
数据结构:JDK 1.7使用Segment数组 + 链表,而JDK 1.8使用数组 + 链表 / 红黑树。红黑树的引入提高了查询效率。
-
扩容机制:两者都支持扩容,但JDK 1.8的扩容机制更为灵活和高效,支持多个线程同时扩容。