并发集合

共 19 题
📑 题目列表 19 题
#
★★★

1. ArrayBlockingQueue 的有界单锁实现

ArrayBlockingQueue 的有界单锁实现是什么?

  • 单锁结构
  • 有界数组
  • 条件变量

ArrayBlockingQueue 用固定数组(有界)+ 单一 ReentrantLock + 两个 Condition(notEmpty/notFull)。put 时获取锁,若数组满则 notFull.await 等待,否则写入并 notEmpty.signal;take 时获取锁,若空则 notEmpty.await,否则取出并 notFull.signal。单锁保护所有操作,保证线程安全。有界数组固定容量,可实现背压。缺点是单锁使 put 与 take 互斥(性能低于双锁的 LinkedBlockingQueue),但实现简单、有界明确。

单锁 + 数组 + 双条件是有界队列的经典实现。单锁保证互斥,双条件保证精确唤醒,有界保证背压。

#
★★★

2. Collections.synchronizedList 与并发集合的取舍

Collections.synchronizedList 与并发集合的取舍是什么?

  • synchronizedList
  • 并发集合
  • 取舍

Collections.synchronizedList 用 synchronized 包裹整个集合,所有方法互斥,简单但并发度低(单锁),迭代需手动加锁(否则快照可能不一致),且复合操作(如 检查-加)需外层同步。并发集合(CopyOnWriteArrayList、ConcurrentLinkedQueue 等)用无锁/分段/写时复制,并发度高、迭代安全(弱一致)。取舍:读多写少、迭代频繁用 CopyOnWriteArrayList;高并发读写用 ConcurrentLinkedQueue/ConcurrentHashMap;简单场景用 synchronizedList。并发集合通常更优。

synchronizedList 是"粗粒度同步",并发集合是"细粒度/无锁"。高并发与弱一致场景用并发集合。

#
★★★

3. ConcurrentHashMap 的 JDK 7/8/9+ 实现演进

ConcurrentHashMap 的 JDK 7/8/9+ 实现演进是什么?

  • JDK 7 分段锁
  • JDK 8 CAS+红黑树
  • JDK 9+ 演进

JDK 7:分段锁(Segment[]),每段一个锁,锁粒度粗,并发度受段数限制。JDK 8:废除分段锁,改为 Node[] + CAS + synchronized(仅锁桶头节点)+ 链表转红黑树(树化),并发度更高、粒度更细、内存更省。JDK 9+:底层迁移到 VarHandle,实现细节优化(如 computeIfAbsent 的 CAS 优化、协助扩容),语义更严谨。演进方向:从"锁分段"到"细粒度 CAS+synchronized",并发度与性能大幅提升。

演进核心是"锁粒度变细":JDK7 分段、JDK8 桶级 CAS+同步、JDK9+ VarHandle。并发度与内存全面提升。

#
★★★

4. ConcurrentHashMap 的 forEach/reduce/search 并行操作

ConcurrentHashMap 的 forEach/reduce/search 并行操作是什么?

  • 并行批量操作
  • 阈值
  • 语义

ConcurrentHashMap 提供 forEach/reduce/search 等并行批量操作(JDK 8),接受一个阈值(parallelism threshold):超过阈值时用 ForkJoinPool 并行执行,否则串行。语义:1) 基于弱一致快照(遍历时可能看到部分变化);2) reduce 需可结合函数(合并);3) search 返回第一个匹配;4) 结果基于扫描时的状态,非精确快照。这些操作用于对大规模 map 做聚合、筛选、搜索,适合只读或容忍弱一致的场景。

并行批量操作基于弱一致遍历 + ForkJoinPool 并行。阈值决定是否并行,reduce 需结合函数。

#
★★★

5. ConcurrentHashMap 的 put/扩容/协助扩容流程

ConcurrentHashMap 的 put/扩容/协助扩容流程是什么?

  • put 流程
  • 扩容流程
  • 协助扩容

put 流程:1) 计算 key 哈希定位桶;2) 桶空则 CAS 插入(无锁);3) 桶非空则 synchronized 锁桶头,遍历链表/树插入或更新;4) 链表长度超阈值(8)则树化;5) 计数(baseCount/CounterCell)。扩容流程:当容量超阈值时触发扩容,扩容采用"多线程协助":复制的线程通过 transfer 协助迁移桶,new Tab 分配后按索引迁移,ForwardingNode 标记已迁移桶。其他线程 put 时发现 ForwardingNode 会协助扩容,保证扩容时写入不阻塞。扩容是分段、多线程协作的。

put 用 CAS+桶锁,扩容用多线程协助(transfer),ForwardingNode 标记迁移状态。这是 CHM 高并发的核心。

#
★★★

6. ConcurrentHashMap 的计数机制(baseCount/CounterCell)

ConcurrentHashMap 的计数机制(baseCount/CounterCell)是什么?

  • baseCount
  • CounterCell
  • 计数准确性与性能

ConcurrentHashMap 用 baseCount + CounterCell[] 计数(类似 Striped64)。低竞争时 CAS 更新 baseCount;竞争激烈时用 CounterCell 分段计数(每个线程哈希到 cell 更新),避免单点竞争。size() 时把 baseCount 与所有 CounterCell 求和(非精确快照,弱一致)。计数机制在保证线程安全的同时,权衡了"映射大小"的精确性与高并发性能。mappingCount() 返回 long 更精确。

计数用 baseCount+CounterCell 分散,size() 是求和近似值。本质是"分段计数 + 合并不精确"。

#
★★★

7. ConcurrentHashMap.keySet() 视图的弱一致性

ConcurrentHashMap.keySet() 视图的弱一致性是什么?

  • 弱一致性
  • keySet 视图
  • 迭代语义

ConcurrentHashMap.keySet()(及 values/entrySet)返回的视图/迭代器是弱一致(weakly consistent)的:迭代时不会抛 ConcurrentModificationException,但迭代器不保证反映迭代开始后的所有修改——可能看到部分修改、可能重复、可能漏掉。同时迭代器基于"遍历时快照",不持有锁。keySet 视图支持对 key 的操作(如 remove、containsKey)反映到 map。适合并发读遍历(如统计、批量处理),但不适合需要严格一致快照的场景。

弱一致 = 迭代安全但不保证快照一致。CHM 迭代器无锁、弱一致,适合并发遍历。

#
★★★

8. ConcurrentHashMap.size() 在分片计数下的近似返回与精确性取舍

ConcurrentHashMap.size() 在分片计数下的近似返回与精确性取舍是什么?

  • size() 近似
  • 分片计数
  • 精确性取舍

size() 通过累加 baseCount 与 CounterCell[] 得到,求和过程可能被并发修改,因此返回的是近似值(弱一致),非精确快照。精确性取舍:为保证 size 精确需加锁或线性化,会牺牲高并发插入性能;CHM 选择"近似 size + 高并发写入",即 size() 有极小误差但 put 性能高。对需要精确大小的场景(如精确计数、判断是否超限),应改用原子计数或允许误差。mappingCount() 返回 long 更精确。

CHM 用"近似 size"换"高并发写入"。精确大小需额外维护,否则用近似值。

#
★★

9. ConcurrentSkipListMap 在有序并发场景下与 Collections.synchronizedSortedMap 的性能对比

ConcurrentSkipListMap 在有序并发场景下与 Collections.synchronizedSortedMap 的性能对比是什么?

  • 跳表
  • synchronizedSortedMap
  • 性能对比

ConcurrentSkipListMap 用跳表(无锁/分段 CAS),支持有序键并发访问,读多写多并发度高,无全局锁,迭代弱一致。Collections.synchronizedSortedMap 用 TreeMap + 全局锁,所有操作互斥,简单但并发度低(单锁),高并发下成为瓶颈。性能对比:高并发读写下 ConcurrentSkipListMap 明显更优(无全局锁、CAS 操作);低并发或简单场景 synchronizedSortedMap 足够。有序并发场景推荐 ConcurrentSkipListMap。

跳表无锁高并发 vs 全局锁低并发。有序高并发用 ConcurrentSkipListMap。

#
★★

10. CopyOnWriteArrayList 在读多写少场景下的写时复制成本与适用边界

CopyOnWriteArrayList 在读多写少场景下的写时复制成本与适用边界是什么?

  • 写时复制
  • 成本
  • 适用边界

CopyOnWriteArrayList 读时不加锁(直接读底层数组),写时复制整个数组(新数组 + volatile 引用替换),读无锁、迭代安全(快照)。成本:写操作 O(n) 复制数组,内存与 CPU 开销大,写放大严重。适用边界:读多写少、迭代频繁、写频率低(如配置、订阅列表);不适合写频繁(每次写复制要 O(n) 且 GC 压力大)或数据量大。读无锁高并发,写是代价。

CopyOnWriteArrayList 用"读无锁、写复制"换"读快、迭代安全"。写频率低才划算。

#
★★

11. LinkedBlockingQueue 的双锁(take/put)实现

LinkedBlockingQueue 的双锁(take/put)实现是什么?

  • 双锁
  • take/put 分离
  • 容量

LinkedBlockingQueue 用两个 ReentrantLock:takeLock(出队)与 putLock(入队),分别保护队头与队尾,配合两个 Condition(notEmpty/notFull)。这样 put 与 take 可并行(不同锁),吞吐高于单锁的 ArrayBlockingQueue。容量可选(默认无界,可指定有界)。双锁实现要点:链接节点(Node),队尾追加、队头取出,计数用 AtomicInteger 保证两锁间一致。有界时满则 notFull.await,空则 notEmpty.await。

双锁分离 put/take,提高并发吞吐。计数用 AtomicInteger 跨锁协调。有界或无界可选。

#
★★

12. 为什么 ThreadPoolExecutor 不推荐使用 Executors.newFixedThreadPool 默认的 LinkedBlockingQueue,无界队列在高并发下会引发哪些典型故障

为什么 ThreadPoolExecutor 不推荐 Executors.newFixedThreadPool 默认的 LinkedBlockingQueue?无界队列在高并发下会引发哪些典型故障?

  • newFixedThreadPool 默认
  • 无界队列
  • 故障

Executors.newFixedThreadPool 默认用无界 LinkedBlockingQueue,任务队列无上限。高并发下任务堆积到无界队列不触发拒绝策略,也不扩张线程(maxPoolSize 无效),可能:1) 队列无限增长导致内存溢出(OOM);2) 任务积压、延迟无限增大,P99 恶化;3) 无法感知反压,生产-消费失衡。不推荐原因:无界队列掩盖了容量问题,故障时表现为内存暴涨而非快速失败。应显式指定有界队列 + 拒绝策略,形成背压。

无界队列是"安全的隐患":不拒绝、不扩容、内存 OOM。应显式用有界队列与控制策略。

#
★★

13. 锁分段(Striped Lock)在 ConcurrentHashMap 早期版本的设计与 JDK 8 后的演进

锁分段(Striped Lock)在 ConcurrentHashMap 早期版本的设计与 JDK 8 后的演进是什么?

  • 分段锁
  • JDK 8 演进
  • 优缺点

早期(JDK 6/7)ConcurrentHashMap 用锁分段(Segment[]):把表分成多段,每段一个锁,写操作只锁对应段,提高并发度(并发度=段数)。缺点:段数固定(默认 16),并发度受限,且段内仍粗粒度。JDK 8 演进:废除 Segment,改为 Node[] + CAS + synchronized(桶级锁),每个桶独立锁/无锁,并发度更高、粒度更细、内存更省。演进使 CHM 从"分段锁"走向"细粒度 CAS+同步"。

分段锁是"锁粒度折中",JDK 8 用桶级 CAS+同步进一步细化。演进方向是并发度更高。

#
★★

14. BlockingQueue 在生产者-消费者中的取舍

BlockingQueue 在生产者-消费者中的取舍是什么?

  • 有界 vs 无界
  • 阻塞语义
  • 背压

BlockingQueue 提供阻塞的 put/take(队满阻塞、队空阻塞),是生产者-消费者的核心。取舍:1) 有界队列(ArrayBlockingQueue/LinkedBlockingQueue(capacity))提供背压,队满时生产者阻塞,避免积压与 OOM,但需处理阻塞与超时;2) 无界队列(LinkedTransferQueue)线程不阻塞但可能内存膨胀;3) 同步队列(SynchronousQueue)无缓冲,直接交接,适合一对一。选择:需背压用有界,需吞吐与低延迟用无界/同步,结合超时与拒绝策略。

队列取舍 = 背压 vs 吞吐 vs 内存。有界保背压,无界防 OOM 需小心,同步队列交接。

#
★★

15. ConcurrentSkipListMap 的跳表实现与时间复杂度

ConcurrentSkipListMap 的跳表实现与时间复杂度是什么?

  • 跳表结构
  • 时间复杂度
  • 无锁

ConcurrentSkipListMap 用跳表(skiplist):多层有序链表,每层是下层的子集,通过随机层数定位,查找时从高层快速下跳。时间复杂度:查找/插入/删除均为 O(log n)。并发实现:无锁,用 CAS 更新节点指针与层,支持并发读写。与红黑树(TreeMap)相比,跳表便于并发(无旋转、易 CAS),支持有序遍历与范围操作。适合需要有序、并发、可扩展的场景。

跳表 O(log n) 操作 + 无锁 CAS 并发,比红黑树更适合并发有序结构。

#
★★

16. ConcurrentSkipListSet 的底层实现

ConcurrentSkipListSet 的底层实现是什么?

  • 底层结构
  • 依托 CHM/跳表
  • 有序集合

ConcurrentSkipListSet 底层依赖 ConcurrentSkipListMap(内部用一个 NavigableMap 实现),元素作为 map 的 key,value 统一为 Boolean.TRUE。它继承跳表的有序性与并发性:O(log n) 并发操作、无锁、有序遍历(sortedSet)、弱一致迭代。提供 add/remove/contains 等集合操作,映射到跳表 map 的 put/remove/containsKey。适合需要"有序、并发、无重复"的集合场景。

ConcurrentSkipListSet 是 ConcurrentSkipListMap 的"value 恒真"封装,复用跳表的有序并发能力。

#
★★

17. DelayQueue 的延迟元素与 ScheduledThreadPoolExecutor

DelayQueue 的延迟元素与 ScheduledThreadPoolExecutor 的关系是什么?

  • DelayQueue
  • 延迟元素
  • ScheduledThreadPoolExecutor

DelayQueue 是延迟队列:元素实现 Delayed(getDelay 返回剩余延迟),只有延迟到期的元素才能被 take 取出(队首按剩余时间排序)。ScheduledThreadPoolExecutor 内部使用 DelayQueue 保存定时任务(ScheduledFutureTask 实现 Delayed),按执行时间排序,到期时才取出执行。因此 DelayQueue 是定时任务调度的核心:take 时阻塞直到队首到期,实现 scheduleAtFixedRate/scheduleWithFixedDelay 的定时触发。两者的关系:ScheduledThreadPoolExecutor 用 DelayQueue 管理定时任务。

DelayQueue 按延迟排序+到期取出,ScheduledThreadPoolExecutor 用它实现定时调度。

#
★★

18. SynchronousQueue 的公平/非公平模式

SynchronousQueue 的公平/非公平模式是什么?

  • SynchronousQueue
  • 公平模式
  • 非公平模式

SynchronousQueue 无缓冲,put 与 take 必须直接交接(一个生产、一个消费配对)。公平模式(构造函数传 true):用 TransferQueue(FIFO 队列)保证先到先服务,交接按顺序,公平但吞吐略低。非公平模式(默认 false):用 TransferStack(栈,LIFO),吞吐高但可能饿死(后进先出)。选择:需要公平、避免饥饿用公平模式;追求吞吐用非公平。SynchronousQueue 常用于线程池 workQueue(Executors.newCachedThreadPool)实现"无缓冲直达"。

SynchronousQueue 公平用 FIFO 队列,非公平用 LIFO 栈。公平 vs 吞吐的选择。

#

19. ConcurrentLinkedQueue 的非阻塞算法(Michael & Scott 队列)在 JDK 25 中的演化

ConcurrentLinkedQueue 的非阻塞算法(Michael & Scott 队列)在 JDK 25 中的演化是什么?

  • Michael & Scott 算法
  • 无锁队列
  • JDK 25 演化

ConcurrentLinkedQueue 基于 Michael & Scott 无锁队列算法:用 CAS 更新 head/tail,尾插入用 CAS 追加节点,头出队用 CAS 摘除,无锁、无阻塞。JDK 25 中算法核心不变,但底层从 Unsafe 迁移到 VarHandle(JDK 9+),实现细节优化(如 tail 的宽松更新),性能与可移植性提升。它适合高并发、无界、无阻塞的 FIFO 队列,用弱一致迭代。JDK 25 的演化主要是底层 API 迁移与微优化,算法仍是无锁 M&S。

M&S 队列是经典无锁 FIFO,JDK 25 用 VarHandle 实现、语义不变。适合高并发无界场景。