【JUC】集合线程安全

Java 中的集合框架大部分默认实现(如 ArrayList、HashMap、HashSet)都不是线程安全的 。在多线程环境下并发修改这些集合,可能导致数据不一致、ConcurrentModificationException,甚至死循环(如 JDK 7 的 HashMap 扩容问题)。

下面从「为什么不安全」「如何变成安全」「有哪些原生安全集合」几个层面来讲。


一、为什么普通集合不安全

以 ArrayList 为例,add 方法核心是:

java 复制代码
elementData[size++] = e;

这行不是原子操作,分三步:读取 size、写入元素、size 加一。多线程同时执行会丢失更新。

HashMap 更严重,并发 put 可能触发扩容时链表成环,导致 get 时 CPU 100%(JDK 8 已改进,但仍不安全)。

另外,迭代器是 fail-fast 的 :遍历过程中如果集合被其他线程修改,会抛 ConcurrentModificationException。注意它只是「尽力检测」,不保证一定发现。


二、把普通集合包装成线程安全

Collections 工具类提供了一批静态方法:

java 复制代码
List<String> list = Collections.synchronizedList(new ArrayList<>());
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
Set<String> set = Collections.synchronizedSet(new HashSet<>());

原理:给每个方法加 synchronized,用同一个 mutex 锁住。

关键坑:复合操作仍需自己加锁。 比如:

java 复制代码
// 错误:check-then-act 不是原子的
if (!list.contains(x)) {
    list.add(x);
}

必须手动同步:

java 复制代码
synchronized (list) {
    if (!list.contains(x)) list.add(x);
}

遍历也必须手动加锁,因为迭代器本身没锁:

java 复制代码
synchronized (list) {
    for (String s : list) { ... }
}

这类包装集合性能一般(全表一把锁),适合低并发场景。


三、java.util.concurrent 下的并发集合

这是高并发场景的推荐选择,按数据结构分类:

1. List / Set

类 特点
CopyOnWriteArrayList 写时复制,读无锁、写加锁并复制整个数组
CopyOnWriteArraySet 基于前者实现,去重

CopyOnWriteArrayList 适合读多写极少 的场景(如监听器列表)。写操作代价高(复制全数组),且迭代器是快照,遍历时看不到最新修改,也不会抛 ConcurrentModificationException。

2. Map

类 特点
ConcurrentHashMap 分段/桶级锁 + CAS,高并发首选
ConcurrentSkipListMap 跳表实现,有序,支持范围查询

ConcurrentHashMap 是重点:

  • JDK 7:Segment 分段锁,默认 16 段。
  • JDK 8+:取消 Segment,改为 CAS + synchronized 锁单个桶,并发度更高。
  • 读操作基本无锁(volatile 保证可见性)。
  • 不允许 null 键和 null 值(因为无法区分「不存在」和「值为 null」)。
  • 复合操作提供了原子方法:putIfAbsent、computeIfAbsent、merge、replace 等,比手动加锁优雅。
java 复制代码
map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);

注意:ConcurrentHashMap 的 size() 返回的是估算值,不保证精确。

3. Queue

类 特点
ConcurrentLinkedQueue 无锁 CAS,高性能无界队列
ArrayBlockingQueue 数组有界,一把锁 + 两个条件
LinkedBlockingQueue 链表,可选有界,读写分离两把锁
PriorityBlockingQueue 带优先级的无界阻塞队列
SynchronousQueue 不存储元素,直接传递
DelayQueue 延迟取元素

BlockingQueue 是生产者-消费者模型的核心,提供 put/take(阻塞)和 offer/poll(带超时)等方法。

4. Deque

ConcurrentLinkedDeque、LinkedBlockingDeque,支持双端操作。


四、怎么选

  • 单线程或方法内局部使用:普通 ArrayList/HashMap 即可,别过度设计。
  • 低并发、要包装已有集合:Collections.synchronizedXxx,但记得复合操作和遍历手动加锁。
  • 高并发读多写少:CopyOnWriteArrayList。
  • 高并发键值存储:ConcurrentHashMap。
  • 生产者-消费者:BlockingQueue 家族。
  • 需要有序并发 Map:ConcurrentSkipListMap。

五、几个常见误区

  1. ConcurrentHashMap 不是所有操作都原子 。get 后再 put 这种复合逻辑仍要用 computeIfAbsent 等原子方法。
  2. synchronizedList 遍历要加锁,很多人漏掉这点。
  3. CopyOnWriteArrayList 不适合写多,每次写都复制数组,内存和性能开销大。
  4. ConcurrentHashMap 不允许 null,迁移代码时要注意。
  5. 线程安全 ≠ 复合操作安全,这是最容易踩的坑。
相关推荐
用户4329359280517 小时前
CoAP还是MQTT:1万只Cat.1水表三个月带宽续航实测
java
知守观17 小时前
多模块架构演进:横向分层 vs 纵向领域,13 个真实项目的切分对照
java·后端·微服务
Java成神之路-17 小时前
Java String 为什么不可变?和 HashMap 到底有什么关系?
java
paopaokaka_luck17 小时前
基于springboot+vue的益暖微光公益服务平台(DeepSeek公益助手、善款信息追溯、志愿服务积分闭环、ECharts运营分析、活动状态自动流转)
java·前端·javascript·spring boot·mysql·架构·echarts
文丑颜不良啊17 小时前
LeetCode1401~1420题目解析
java·leetcode
Nuanyt17 小时前
LeetCode Hot 100 链表专题2 3 题 (Java)
java·笔记·算法·leetcode·链表
文丑颜不良啊18 小时前
LeetCode1341~1360题目解析
java·leetcode
QianhangQianping18 小时前
撤销重做不是「存快照」:一个可视化编辑器的Operation 设计
java·前端·数据库·typescript·编辑器·前端架构·撤销重做
海绵宝宝转agent18 小时前
LeetCodehot100题解
java·笔记·学习·算法·leetcode·深度优先
SunnyDays101118 小时前
Java 操作 Word 脚注和尾注:添加、读取与删除
java·word