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。
五、几个常见误区
ConcurrentHashMap不是所有操作都原子 。get后再put这种复合逻辑仍要用computeIfAbsent等原子方法。synchronizedList遍历要加锁,很多人漏掉这点。CopyOnWriteArrayList不适合写多,每次写都复制数组,内存和性能开销大。ConcurrentHashMap不允许 null,迁移代码时要注意。- 线程安全 ≠ 复合操作安全,这是最容易踩的坑。