生产者消费者模型离不了阻塞队列,读多写少场景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。