☰
10-除了 ConcurrentHashMap,这些并发容器和阻塞队列也该会
2026/9/25 17:19:41 网站建设 项目流程

生产者消费者模型离不了阻塞队列,读多写少场景CopyOnWrite比加锁香,高频插入用无锁队列更猛。这一篇把 JUC 里常用的并发容器和阻塞队列全家桶补齐,让你在合适场景直接掏出对的那个,而不是无脑synchronized一把锁。

一、为什么不能用普通集合做并发

HashMap、ArrayList都不是线程安全的,多线程下:

  • HashMap并发put可能死循环(JDK 7 及之前头插法扩容)、数据丢失、size 错乱。
  • ArrayList并发add可能数组越界或元素覆盖。

老办法是Collections.synchronizedMap/list(给每个方法包synchronized),但读写全互斥,并发度低。JUC 的并发容器用更聪明的策略:分段锁、CAS、写时复制,读写可并行。

二、ConcurrentHashMap:并发 Map 的标准答案

JDK 8 起用「数组 + 链表/红黑树 + CAS +synchronized锁桶头」实现,读几乎无锁,写只锁单个桶,并发度极高。注意它的复合操作不是原子的:

// 错误:putIfAbsent 是原子的,但「判断+放」两句不是if(!map.containsKey(k))map.put(k,v);// 多线程下可能重复放// 正确:用原子复合方法map.putIfAbsent(k,v);map.computeIfAbsent(k,key->load(key));// 存在则不计算,天然防重复

computeIfAbsent还顺带解决了「双重检查 + 初始化」的竞态,是缓存懒加载的利器。

三、CopyOnWriteArrayList / CopyOnWriteArraySet:读多写少专属

原理:写时复制。修改时复制一份底层数组,在新数组上改,改完把引用指向新数组;读永远读旧数组,读写互不阻塞。

CopyOnWriteArrayList<String>list=newCopyOnWriteArrayList<>();list.add("a");// 复制数组+追加,开销大list.get(0);// 直接读,无锁,超快

适用场景极明确:读远多于写(如监听器列表、配置白名单、路由表)。缺点也明显:

  • 每次写都复制整个数组,写慢、占内存(存在新旧两份瞬时副本)。
  • 弱一致性:读到的可能是修改前的快照,不适合强实时。

千万别用来做「频繁 add 的队列」,那就变成内存杀手。

四、ConcurrentLinkedQueue:高并发无锁队列

基于 Michael-Scott 算法的非阻塞无锁队列,用 CAS 实现入队出队,高并发下比阻塞队列吞吐更高(因为没有锁竞争和线程挂起)。

ConcurrentLinkedQueue<String>q=newConcurrentLinkedQueue<>();q.offer("task");// 入队(CAS,非阻塞)Stringt=q.poll();// 出队,空返回 null

适用:高并发「提交即返回」、不需要「队列空就等」的场景(如日志异步收集、事件总线)。它不支持阻塞等待,要阻塞请用下面的阻塞队列。

五、阻塞队列 BlockingQueue 全家桶

BlockingQueue是生产者-消费者模型的骨架:队空时取操作阻塞,队满时放操作阻塞。这是线程池(workQueue)、ArrayBlockingQueue背后的核心接口。

实现底层有界?特点
ArrayBlockingQueue数组有界(构造必填容量)单锁,公平可选,最常用
LinkedBlockingQueue链表可选(不填=无界Integer.MAX)双锁(入队出队分离),吞吐高
SynchronousQueue无缓冲0 容量提交必须有人立刻接,否则等;线程池缓存池用它
PriorityBlockingQueue堆无界按优先级出队
DelayQueue堆无界按延迟时间出队,到期才能取
LinkedTransferQueue链表无界融合SynchronousQueue+队列,transfer直交

典型生产者消费者:

BlockingQueue<Task>q=newArrayBlockingQueue<>(100);// 生产者newThread(()->{while(true)q.put(produce());}).start();// 满则阻塞// 消费者newThread(()->{while(true)consume(q.take());}).start();// 空则阻塞

注意LinkedBlockingQueue不指定容量就是无界,和线程池配一起有 OOM 风险(第 7 篇提过),务必指定容量。

几个细分用法

  • DelayQueue:实现「延迟任务调度」,如订单 30 分钟未支付自动关单——元素实现Delayed,take()等到过期时间才返回。
  • SynchronousQueue:容量 0,生产者put必须等消费者take接手。适合「直接交接」场景,Executors.newCachedThreadPool就是它。
  • PriorityBlockingQueue:按Comparable优先级出队,无界,注意内存。

六、ConcurrentSkipListMap / Set:并发且有序

需要「并发 + 按 key 排序」,用跳表实现的ConcurrentSkipListMap(对应非并发的TreeMap)。它支持range视图(subMap),适合并发环境下的范围查询/时间序列。

七、选型速查

场景用什么
并发 KV 缓存/映射ConcurrentHashMap
读极多写极少(监听器/白名单)CopyOnWriteArrayList/Set
高并发提交、不需阻塞ConcurrentLinkedQueue
生产者-消费者(要阻塞)ArrayBlockingQueue(有界)/LinkedBlockingQueue(指定容量)
延迟任务/定时DelayQueue
直接交接SynchronousQueue
并发且有序ConcurrentSkipListMap/Set

八、一个总原则

并发容器不是「把HashMap换成ConcurrentHashMap就完事」。想清楚三件事:是否真的共享且并发?读写比如何?复合操作要不要原子?读多写少选CopyOnWrite,要阻塞选阻塞队列,纯高并发入队选无锁队列,K-V 选 CHM。选错比不加锁还糟——比如把CopyOnWriteArrayList当高频写队列用,直接内存爆炸。

总结

并发容器按场景选:ConcurrentHashMap解决并发 KV(注意复合操作要用putIfAbsent/computeIfAbsent才原子);读多写少用CopyOnWrite(写时复制,读写不互斥,弱一致);高并发非阻塞入队用ConcurrentLinkedQueue;生产者-消费者用BlockingQueue(ArrayBlockingQueue有界最安全、DelayQueue做延迟、SynchronousQueue零缓冲交接);并发有序用ConcurrentSkipListMap。LinkedBlockingQueue务必指定容量防 OOM。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询