并发集合总览
当应用进入多线程场景,传统的 ArrayList、HashMap、HashSet 已无法保证线程安全。JDK 在 java.util.concurrent(JUC)中提供了大量并发友好的集合实现,它们通过无锁算法、分段锁、写时复制或阻塞队列等机制,确保在高并发下仍能安全高效地访问数据。
为什么需要并发集合?🤔
- 避免数据竞争:多线程同时读写共享集合会导致数据错乱甚至崩溃。
- 提升性能:相比整集合加
synchronized,并发集合采用更细粒度的锁或无锁策略,吞吐更高。 - 易用性:提供现成的 API(如
putIfAbsent、computeIfAbsent、offer)简化并发编程。 - 语义更明确:例如阻塞队列天然支持生产者-消费者模式。
并发集合族谱 🌳
| 类别 | 典型实现 | 关键特性 | 常见场景 |
|---|---|---|---|
| 并发 Map | ConcurrentHashMap、ConcurrentSkipListMap | 分段锁/无锁、遍历弱一致性 | 缓存、计数器、配置中心 |
| 并发 List | CopyOnWriteArrayList | 写时复制、读无锁 | 黑名单、事件监听器、读多写少 |
| 并发 Set | CopyOnWriteArraySet、ConcurrentHashMap.newKeySet() | 基于 CopyOnWrite 或 CHM | 在线用户集合、广播订阅 |
| 非阻塞队列 | ConcurrentLinkedQueue、ConcurrentLinkedDeque | CAS 链表、无界非阻塞 | 任务池、事件队列 |
| 阻塞队列 | ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue | 生产者-消费者、容量控制 | 线程池、异步日志 |
| 延迟与优先队列 | DelayQueue、PriorityBlockingQueue | 按时间/优先级出队 | 延迟任务、定时器 |
| 并发跳表 | ConcurrentSkipListMap、ConcurrentSkipListSet | 有序、lock-free | 排序视图、范围查询 |
常见设计策略 🧠
- 分段锁(Segmented Lock):把大锁拆成多把小锁,老版
ConcurrentHashMap用Segment,JDK8 则直接在桶节点加锁,核心思想是“缩小临界区”。 - CAS + 自旋:利用 CPU 的 Compare-And-Swap 原语乐观更新指针或计数器,如
ConcurrentLinkedQueue,失败就自旋重试,无需阻塞。 - 写时复制(Copy-On-Write):写操作复制底层数组,修改后一次性替换引用,读操作完全无锁,如
CopyOnWriteArrayList/Set。 - 阻塞/唤醒:
BlockingQueue通过ReentrantLock + Condition在队列满或空时await,状态改变后signal唤醒线程,天然具备背压能力。 - 弱一致性迭代:遍历期间允许结构变化,如
ConcurrentHashMap、ConcurrentSkipListMap,不抛 ConcurrentModificationException,但可能看不到最新数据。 - 多层索引:跳表、双端队列在不同维度上拆分锁或采用 CAS,兼顾有序与并发。
使用建议 ✅
- 读远多于写:优先
CopyOnWriteArrayList/Set - 无序高速缓存:用
ConcurrentHashMap - 需要顺序/范围操作:
ConcurrentSkipListMap/Set - 生产者-消费者:阻塞队列(容量根据场景设计)
- 事件广播:
CopyOnWriteArrayList存放监听器 - 异步协作:
ConcurrentLinkedQueue配合自定义线程
API 小抄 📎
// CopyOnWrite
CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>();
listeners.addIfAbsent("handler");
// Concurrent Set
Set<String> users = ConcurrentHashMap.newKeySet();
users.add("Tom");
// 阻塞队列
BlockingQueue<String> queue = new LinkedBlockingQueue<>(100);
queue.put("task"); // 满则阻塞
String task = queue.take(); // 空则阻塞
// 并发跳表
ConcurrentSkipListMap<Long, String> schedule = new ConcurrentSkipListMap<>();
schedule.put(System.currentTimeMillis() + 1000, "job");
schedule.firstEntry();
场景拆解 🧱
- 配置中心/灰度发布:核心配置存
ConcurrentHashMap,监听器列表用CopyOnWriteArrayList,变更时无锁广播。 - 实时指标统计:
ConcurrentHashMap<String, LongAdder>组合,写入无锁累加,定时任务再快照输出。 - 消息分发/事件总线:
LinkedTransferQueue或ConcurrentLinkedQueue作为缓冲,消费者线程池异步处理,必要时搭配Semaphore限流。 - 热点缓存淘汰:单线程可用
LinkedHashMap实现 LRU,多线程则采用 Caffeine、GuavaCacheBuilder或自研基于ConcurrentHashMap+LinkedBlockingQueue。
高频面试题 🔥
-
为什么不用
Collections.synchronizedList()?
它对所有读写都加同一把锁,锁竞争剧烈,遍历时还需开发者额外同步;CopyOnWriteArrayList或ReadWriteLock + ArrayList能显著提升读性能。 -
弱一致性迭代器是什么?
ConcurrentHashMap、ConcurrentSkipListMap的迭代器读取的是创建瞬间的结构快照,不会抛异常,但也不保证遍历期间看到最新数据,适合监控、统计等“尽力而为”场景。 -
CopyOnWrite 的缺点?
写操作需要复制整份数组,延迟高且瞬时内存翻倍;此外迭代器看到的是旧数据,不适合需要实时一致性的场景。 -
阻塞队列与非阻塞队列的差异?
阻塞队列在空或满时会挂起线程,适合生产者-消费者和限流;非阻塞队列通过 CAS 快速失败与重试,延迟低但无法自动限流,需要配合自定义背压。 -
如何选择合适的并发集合?
评估四个维度:读写比例、是否要顺序/排序、是否需要阻塞语义、数据规模。根据结果在 CHM、COW、跳表、阻塞队列之间做权衡。
小结
- 并发集合通过多种机制在“安全”与“性能”之间取得平衡。
- 按“读写比例 + 顺序需求 + 容量控制 + 阻塞语义”选择最合适的实现。
- 搭配线程池、原子类、锁等工具使用,才能构建可靠的并发程序。