【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. 线程安全 ≠ 复合操作安全,这是最容易踩的坑。
相关推荐
starzy19902 小时前
Flink SlotManager启动流程源码深度剖析:从ResourceManager到Slot分配的完整链路
java·大数据·flink
极光通讯2 小时前
服务器内存来料检验(IQC)实操:批次追溯、外观判定与上机验证
java·服务器·算法
4SAPI2 小时前
AI API Gateway平台哪个好?从架构视角看企业多模型网关选型与实践
java·大数据·人工智能·gateway·php
EatFan2 小时前
2026 后端 AI 工程化:Spring AI 2.0、MCP 协议与 Agent 内嵌如何收进 Java 生产系统
java·人工智能·spring·agent·spring ai·spring boot 3·mcp
JPower_mr.g2 小时前
SmartCall 音色管理技术解析:基于 SPI 的可扩展音色注册架构
java·开发语言·人工智能·ai·架构·开源
测试开发Kevin2 小时前
IDEA工程结构解析:项目、模块、库、Facet、Artifact (工件) 概念说明
java·ide·intellij idea
大侠归来2 小时前
Spring Boot 2.1 → 3.5 迁移推演:从实战出发的完整路线图
java·spring boot·后端
Wang's Blog2 小时前
Java 中间件之 RabbitMQ 快速入门: SpringAMQP 的 DirectExchange 路由模式
java·中间件·java-rabbitmq
Sweet锦3 小时前
jDCS 开源项目:面向工业现场的 Modbus RTU 数据采集基础框架
java·spring boot·物联网·开源