跳到主要内容

并发集合总览

当应用进入多线程场景,传统的 ArrayListHashMapHashSet 已无法保证线程安全。JDK 在 java.util.concurrent(JUC)中提供了大量并发友好的集合实现,它们通过无锁算法、分段锁、写时复制或阻塞队列等机制,确保在高并发下仍能安全高效地访问数据。


为什么需要并发集合?🤔

  • 避免数据竞争:多线程同时读写共享集合会导致数据错乱甚至崩溃。
  • 提升性能:相比整集合加 synchronized,并发集合采用更细粒度的锁或无锁策略,吞吐更高。
  • 易用性:提供现成的 API(如 putIfAbsentcomputeIfAbsentoffer)简化并发编程。
  • 语义更明确:例如阻塞队列天然支持生产者-消费者模式。

并发集合族谱 🌳

类别典型实现关键特性常见场景
并发 MapConcurrentHashMapConcurrentSkipListMap分段锁/无锁、遍历弱一致性缓存、计数器、配置中心
并发 ListCopyOnWriteArrayList写时复制、读无锁黑名单、事件监听器、读多写少
并发 SetCopyOnWriteArraySetConcurrentHashMap.newKeySet()基于 CopyOnWrite 或 CHM在线用户集合、广播订阅
非阻塞队列ConcurrentLinkedQueueConcurrentLinkedDequeCAS 链表、无界非阻塞任务池、事件队列
阻塞队列ArrayBlockingQueueLinkedBlockingQueueSynchronousQueue生产者-消费者、容量控制线程池、异步日志
延迟与优先队列DelayQueuePriorityBlockingQueue按时间/优先级出队延迟任务、定时器
并发跳表ConcurrentSkipListMapConcurrentSkipListSet有序、lock-free排序视图、范围查询

常见设计策略 🧠

  1. 分段锁(Segmented Lock):把大锁拆成多把小锁,老版 ConcurrentHashMapSegment,JDK8 则直接在桶节点加锁,核心思想是“缩小临界区”。
  2. CAS + 自旋:利用 CPU 的 Compare-And-Swap 原语乐观更新指针或计数器,如 ConcurrentLinkedQueue,失败就自旋重试,无需阻塞。
  3. 写时复制(Copy-On-Write):写操作复制底层数组,修改后一次性替换引用,读操作完全无锁,如 CopyOnWriteArrayList/Set
  4. 阻塞/唤醒BlockingQueue 通过 ReentrantLock + Condition 在队列满或空时 await,状态改变后 signal 唤醒线程,天然具备背压能力。
  5. 弱一致性迭代:遍历期间允许结构变化,如 ConcurrentHashMapConcurrentSkipListMap,不抛 ConcurrentModificationException,但可能看不到最新数据。
  6. 多层索引:跳表、双端队列在不同维度上拆分锁或采用 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> 组合,写入无锁累加,定时任务再快照输出。
  • 消息分发/事件总线LinkedTransferQueueConcurrentLinkedQueue 作为缓冲,消费者线程池异步处理,必要时搭配 Semaphore 限流。
  • 热点缓存淘汰:单线程可用 LinkedHashMap 实现 LRU,多线程则采用 Caffeine、Guava CacheBuilder 或自研基于 ConcurrentHashMap + LinkedBlockingQueue

高频面试题 🔥

  1. 为什么不用 Collections.synchronizedList()
    它对所有读写都加同一把锁,锁竞争剧烈,遍历时还需开发者额外同步;CopyOnWriteArrayListReadWriteLock + ArrayList 能显著提升读性能。

  2. 弱一致性迭代器是什么?
    ConcurrentHashMapConcurrentSkipListMap 的迭代器读取的是创建瞬间的结构快照,不会抛异常,但也不保证遍历期间看到最新数据,适合监控、统计等“尽力而为”场景。

  3. CopyOnWrite 的缺点?
    写操作需要复制整份数组,延迟高且瞬时内存翻倍;此外迭代器看到的是旧数据,不适合需要实时一致性的场景。

  4. 阻塞队列与非阻塞队列的差异?
    阻塞队列在空或满时会挂起线程,适合生产者-消费者和限流;非阻塞队列通过 CAS 快速失败与重试,延迟低但无法自动限流,需要配合自定义背压。

  5. 如何选择合适的并发集合?
    评估四个维度:读写比例、是否要顺序/排序、是否需要阻塞语义、数据规模。根据结果在 CHM、COW、跳表、阻塞队列之间做权衡。


小结

  • 并发集合通过多种机制在“安全”与“性能”之间取得平衡。
  • 按“读写比例 + 顺序需求 + 容量控制 + 阻塞语义”选择最合适的实现。
  • 搭配线程池、原子类、锁等工具使用,才能构建可靠的并发程序。