11 · 并发容器全家桶
一句话定位:除了 ConcurrentHashMap,JUC 还提供了一整套线程安全容器,各有适用场景。
1. 为什么不用 Collections.synchronizedXxx
List<String>list=Collections.synchronizedList(newArrayList<>());- 本质是给每个方法加
synchronized,整个容器一把锁,并发度=1。 - 复合操作仍不安全(遍历时要手动加锁)。
- 性能差,JUC 容器是更好的选择。
2. CopyOnWriteArrayList:写时复制
核心思想:读不加锁,写的时候复制一份新数组改,改完替换引用。
CopyOnWriteArrayList<String>list=newCopyOnWriteArrayList<>();// add 源码精髓publicbooleanadd(Ee){synchronized(lock){// 写加锁Object[]old=getArray();Object[]newArr=Arrays.copyOf(old,old.length+1);// 复制新数组newArr[old.length]=e;setArray(newArr);// volatile 替换引用returntrue;}}publicEget(intindex){return(E)getArray()[index];// 读完全不加锁!}| 优点 | 缺点 |
|---|---|
| 读无锁,读性能极高 | 写要复制整个数组,内存+性能开销大 |
| 遍历时不抛 ConcurrentModificationException | 数据弱一致(读可能读到旧快照) |
适用:读多写极少、数据量不大。典型:监听器列表、配置白名单、路由表。
不适用:频繁写、大数据量(每次写都复制,OOM 风险)。
3. CopyOnWriteArraySet
基于 CopyOnWriteArrayList 实现的 Set,特性相同,适合读多写少的集合去重场景。
4. ConcurrentLinkedQueue:无锁队列
基于CAS的无界非阻塞队列(不是阻塞队列):
ConcurrentLinkedQueue<Task>queue=newConcurrentLinkedQueue<>();queue.offer(task);// 入队(CAS,不阻塞)Taskt=queue.poll();// 出队,空返回 null(不阻塞)- 高并发下性能好(无锁 CAS)。
- 空时
poll返回 null,不阻塞(和 BlockingQueue 的 take 不同)。 size()要遍历,是 O(n),别频繁调用。
适用:高并发、不需要阻塞等待的队列场景。需要阻塞用 BlockingQueue(见 12 篇)。
5. ConcurrentSkipListMap / Set:并发有序容器
跳表(SkipList)实现的并发有序Map/Set,相当于并发版的 TreeMap。
ConcurrentSkipListMap<Integer,String>map=newConcurrentSkipListMap<>();map.put(3,"c");map.put(1,"a");map.put(2,"b");// 自动按 key 排序,支持范围查询map.firstKey();// 1map.headMap(2);// 小于 2 的部分map.ceilingKey(2);// >= 2 的最小 key跳表结构(多层索引链表,查询 O(log n)): L2: 1 ────────────→ 9 L1: 1 ──→ 4 ──────→ 9 L0: 1 → 3 → 4 → 6 → 9 (底层完整链表)| TreeMap | ConcurrentSkipListMap | |
|---|---|---|
| 线程安全 | ❌ | ✅ |
| 实现 | 红黑树 | 跳表 |
| 有序 | ✅ | ✅ |
适用:需要并发 + 有序 + 范围查询。比如排行榜、按时间排序的事件表。
6. 容器选型速查
| 需求 | 推荐容器 |
|---|---|
| 并发 Map | ConcurrentHashMap |
| 并发有序 Map | ConcurrentSkipListMap |
| 读多写极少的 List | CopyOnWriteArrayList |
| 高并发非阻塞队列 | ConcurrentLinkedQueue |
| 需要阻塞的队列(生产消费) | BlockingQueue(见 12 篇) |
| 并发 Set | ConcurrentHashMap.newKeySet() |
| 读多写少的 Set | CopyOnWriteArraySet |
7. 踩坑与最佳实践
- ⚠️ CopyOnWrite 系列写代价大,只用于读多写极少、数据量小的场景,否则 OOM。
- ⚠️ CopyOnWrite 读的是快照,弱一致(刚写的可能读不到),别用于强一致场景。
- ⚠️
ConcurrentLinkedQueue.size()是 O(n),判空用isEmpty()不要用size()==0。 - ✅ 需要并发 Set 用
ConcurrentHashMap.newKeySet(),比 CopyOnWriteArraySet 更通用。 - ✅ 别用
Collections.synchronizedXxx,JUC 容器性能更好。 - 💡 遍历 CopyOnWriteArrayList 时其他线程的修改不影响当前遍历(基于快照),不会抛并发修改异常。
8. 本篇小结
- CopyOnWriteArrayList:写时复制,读无锁,适合读多写极少。
- ConcurrentLinkedQueue:CAS 无锁队列,不阻塞。
- ConcurrentSkipListMap:跳表实现的并发有序 Map。
- 按“是否有序、是否阻塞、读写比例”选择合适容器。