ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

10-并发容器全景图:除了 ConcurrentHashMap 还有这些得会

10-并发容器全景图:除了 ConcurrentHashMap 还有这些得会

COW、无锁队列、跳表,并发全家福一次认全

提到并发容器,很多人只想到ConcurrentHashMap。其实java.util.concurrent包里还有一整套为多线程打磨的容器:写时复制的 List、无锁链表队列、跳表 Map/Set、各种阻塞队列。这一篇把"并发容器全家福"铺开,讲清每个的底层思路和适用边界,让你在选型时胸有成竹。

一、为什么不能用同步包装就完事?

上一讲提过Collections.synchronizedList/Map/Set能给普通容器加锁,但它是"整表一把锁",迭代还要你手动同步,高并发下吞吐差。并发容器(JCU 包)走的是另一条路:用更细的锁、CAS、写时复制、无锁算法,在不牺牲安全的前提下保住并发度。下面逐个看。

二、CopyOnWriteArrayList:读多写少的读锁自由

原理复习:写时复制整份数组,读无锁。

CopyOnWriteArrayList<String>list=newCopyOnWriteArrayList<>();list.add("a");// 复制整份数组,O(n),贵Stringv=list.get(0);// 无锁,O(1),快for(Strings:list){...}// 迭代器拿快照,绝不 CME

适用:监听器列表、配置项、白名单、路由表这类"初始化后极少改、天天大量读"的数据。绝对别用于高频写(每次写都拷全数组,内存和 CPU 双爆)。CopyOnWriteArraySet就是基于它实现的 Set(去重能力来自底层 List 的addIfAbsent包含检查)。

三、ConcurrentLinkedQueue:无锁的 CAS 链表队列

高并发下不想用阻塞队列(不想让线程挂起)、又要线程安全的 FIFO,就用它:

ConcurrentLinkedQueue<Task>q=newConcurrentLinkedQueue<>();q.offer(task);// CAS 入队,不阻塞Taskt=q.poll();// CAS 出队,空返回 null

底层是Michael-Scott 算法的无锁单向链表,入队出队全程用 CAS 自旋,不阻塞任何线程。特性:

  • 无界、不允许 null、迭代弱一致、size() 弱一致(要遍历计数,O(n))。
  • 高并发非阻塞场景下吞吐优于锁队列;但单线程或低并发时,因 CAS 自旋开销反而可能不如 ArrayDeque。
  • 适合"高并发投递、消费者自己决定何时取"的解耦场景,比如日志异步收集、事件总线。

四、ConcurrentHashMap:并发 Map 标杆

前文专篇讲过。锁定级:CAS + 锁桶头,读无锁,弱一致迭代,分段计数。并发键值存储首选,缓存、计数器、去重表都靠它。记住它不允许 null、size 弱一致、用computeIfAbsent做"缺失才计算"的缓存。

五、ConcurrentSkipListMap / ConcurrentSkipListSet:有序并发

TreeMap线程不安全,要"并发 + 有序"就上跳表:

ConcurrentSkipListMap<Integer,String>map=newConcurrentSkipListMap<>();map.put(10,"a");map.put(20,"b");map.floorKey(15);// 10map.subMap(10,20);// 范围视图

底层是跳表(Skip List)——在有序链表上"加多层索引",查找平均 O(log n),且天然适合无锁并发(相比红黑树,跳表的范围操作和后继定位在 CAS 下更易实现、更少重平衡)。

特性:

  • 实现了ConcurrentNavigableMap,支持subMap、headMap、floorKey、ceilingKey等(同 TreeMap 的能力)。
  • key 必须可比较,不允许 null。
  • 比ConcurrentHashMap慢一档(有排序开销),但换来了"并发下也能范围/邻近查询"。
  • ConcurrentSkipListSet= 基于它的 Set。

适用:并发环境下的排行榜、时间线、区间统计——既要线程安全又要按 key 排序或取前驱后继。

六、阻塞队列家族(并发容器的另一半)

前文队列篇讲过,BlockingQueue的五个实现本身就是并发容器:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue、SynchronousQueue。它们是生产者-消费者、线程池任务队列的主力,这里不再赘述,只强调一句:线程池稳定性直接取决于队列选型(无界队列 OOM、SynchronousQueue 炸线程)。

七、选型速查表(收藏级)

并发需求单线程(非并发)并发容器
List 读多写少ArrayListCopyOnWriteArrayList
List 读写均衡ArrayListsynchronizedList(或外部锁)
Map 键值HashMapConcurrentHashMap
Map 有序TreeMapConcurrentSkipListMap
Set 去重HashSet无并发 Set 标杆,可用 ConcurrentHashMap.newKeySet() 或 CopyOnWriteArraySet
队列 FIFO 非阻塞ArrayDequeConcurrentLinkedQueue
队列 阻塞传递LinkedList 作 Queue各 BlockingQueue
栈ArrayDeque无专门并发栈,用 ConcurrentLinkedDeque(双端)

小技巧:ConcurrentHashMap.newKeySet()能直接拿到一个并发安全的Set(JDK 8 起),底层就是 CHM 的 key 视图,比自己包CopyOnWriteArraySet更轻量,适合高并发去重。

八、弱一致性:并发容器的"潜规则"

几乎所有 JUC 容器(CHM、COW、跳表、并发队列)的迭代器都是弱一致的:遍历开始后,其他线程的增删可能反映、也可能不反映到本次遍历,但绝不抛ConcurrentModificationException。这和HashMap/ArrayList的 fail-fast(一改就抛 CME)正相反。

这带来两个务实影响:

  • 别在遍历时依赖"一定看到最新写入"——并发下没有"即时可见"的保证。要强一致请加锁或用synchronized。
  • 边遍历边修改是安全的(尤其 COW),不会崩。这是并发容器比同步包装好用的一大原因。

九、常见误用

  1. 高频写还用 CopyOnWriteArrayList:每次写复制全数组。日志上报若 QPS 高,应换ConcurrentLinkedQueue或批量落盘。
  2. 把 ConcurrentHashMap 当精确计数器:size()弱一致。要并发计数请用LongAdder(JUC 专为高并发计数设计,分段累加,比AtomicLong在高争用下快得多)。
  3. 用 ConcurrentHashMap 存可变对象当缓存却不控制失效:CHM 不替你做过期淘汰,要做带 TTL 的缓存请用 Caffeine/Guava Cache,或自己加DelayQueue清理。
  4. size()/isEmpty() 当业务判据:弱一致,临界判断要用外部同步或换LongAdder/AtomicInteger。

十、面试追问

  • Q:ConcurrentHashMap 和 ConcurrentSkipListMap 怎么选?不需要排序、追求极致并发和 O(1) 查找 → CHM;需要 key 排序、范围查询、前驱后继 → 跳表。后者慢一档但能力互补。
  • Q:为什么跳表比红黑树更适合并发?红黑树插入要旋转、结构调整复杂,CAS 下极易冲突;跳表只在局部层级改指针、冲突面小,无锁实现简单且范围遍历高效。
  • Q:CopyOnWriteArrayList 的迭代器能反映写入吗?不能,它持有创建时的数组快照,弱一致。但这正是它遍历安全、不抛 CME 的原因。

总结

并发容器全家福:CopyOnWriteArrayList/Set(读多写少、写时复制)、ConcurrentLinkedQueue(无锁 CAS 队列)、ConcurrentHashMap(并发 Map 标杆)、ConcurrentSkipListMap/Set(并发有序、跳表)、各 BlockingQueue(阻塞传递)。记住"弱一致迭代、size 不精确、CAS/写时复制/跳表"三条主线,按"读写比例、是否需要排序、是否跨线程"三问选型,就能在并发集合里不踩坑。HashMap 的并发版是 CHM,TreeMap 的并发版是跳表,ArrayList 读多写少版是 COW——整条映射关系一眼可推。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进