面试题答案
一键面试1. 触发条件
- 容量达到阈值:
ConcurrentHashMap
内部有一个负载因子(默认为0.75)和一个阈值(容量 * 负载因子)。当ConcurrentHashMap
中元素数量达到阈值时,就会触发扩容。例如,初始容量为16,负载因子为0.75,那么阈值就是16 * 0.75 = 12,当元素数量达到12时,就可能触发扩容。 - 并发插入导致的扩容:在高并发场景下,多个线程同时进行插入操作,如果发现当前桶(bucket)正在进行初始化(即该桶的节点为
null
且正在被其他线程初始化),当前线程会帮助进行初始化;如果发现当前桶正在扩容,当前线程也会帮助进行扩容。
2. 扩容过程
- 构建新的数组:扩容时,
ConcurrentHashMap
会创建一个新的Node
数组,新数组的大小是原数组大小的2倍。例如,原数组大小为16,扩容后新数组大小为32。 - 迁移数据:将原数组中的数据迁移到新数组中。迁移过程采用分段(segment)的方式,每个线程负责迁移一段数据。具体步骤如下:
- 首先遍历原数组,对每个桶(bucket)进行操作。
- 对于每个桶,根据其节点类型进行不同处理:
- 普通链表:采用头插法将链表中的节点重新计算哈希值后插入到新数组对应的桶中。由于是头插法,在多线程环境下可能会导致链表成环,但
ConcurrentHashMap
采用了一些机制避免这种情况。 - 红黑树:先将红黑树转换为链表,再按照普通链表的方式进行迁移,迁移完成后如果链表长度大于8且新数组容量大于64,会将链表重新转换为红黑树。
- 普通链表:采用头插法将链表中的节点重新计算哈希值后插入到新数组对应的桶中。由于是头插法,在多线程环境下可能会导致链表成环,但
- 迁移完成后,更新相关的指针和元数据。
3. 对高并发场景下性能的影响
- 性能抖动:扩容过程中,由于需要迁移大量数据,会占用较多的CPU和内存资源,导致应用程序在扩容期间性能出现抖动,响应时间变长。
- 竞争加剧:在高并发环境下,多个线程同时参与扩容,可能会导致线程竞争加剧,进一步降低系统性能。例如,多个线程同时操作同一个桶(bucket)时,会产生锁竞争,影响并发效率。
4. 优化措施
- 预初始化:在应用启动时,根据预估的数据量,提前设置合适的初始容量,尽量减少运行时的扩容次数。例如,如果预估数据量为1000,负载因子为0.75,那么初始容量至少设置为1000 / 0.75 ≈ 1334,向上取整为1344(最好是2的幂次方,可设置为2048)。
- 合理设置线程数:在
ConcurrentHashMap
中,可以通过设置合适的并行度(concurrencyLevel
)来控制并发访问的线程数。如果线程数过多,会增加线程切换开销和竞争;如果线程数过少,又无法充分利用多核CPU的优势。一般情况下,可以根据服务器的CPU核心数来设置并行度,例如CPU核心数为8,并行度可设置为8左右。 - 使用读写锁优化读操作:虽然
ConcurrentHashMap
读操作一般不需要加锁,但在扩容期间,读操作可能会受到影响。可以考虑使用读写锁,在读多写少的场景下,提高读操作的并发性能。在扩容时,写操作加写锁,读操作加读锁,避免读操作在扩容期间读到不一致的数据。 - 异步扩容:可以考虑将扩容操作异步化,例如使用
CompletableFuture
或线程池来执行扩容任务,减少扩容对主线程的影响。在异步扩容过程中,要注意处理好数据一致性问题,确保在扩容期间读写操作的正确性。