Java集合框架多线程安全的核心问题
Java集合框架提供了丰富的集合实现,比如List体系的ArrayList、LinkedList,Map体系的HashMap、TreeMap,Set体系的HashSet、TreeSet等,这些集合在设计时都没有考虑多线程并发场景,因此都是非线程安全的。在多线程同时读写这些集合时,很容易出现各类问题。
最常见的问题是并发修改异常,比如一个线程在遍历ArrayList,另一个线程同时修改ArrayList的结构(新增、删除元素),就会抛出ConcurrentModificationException。另外还会出现数据覆盖、数据丢失、内部结构损坏等问题,比如多线程同时往HashMap中put元素,可能会导致链表成环,查询时出现死循环,或者元素没有被正确存入集合。
因此当我们需要在多线程场景下使用集合时,必须采取额外措施保证集合的线程安全,下面介绍几种常用的实现方式。
使用同步容器保证线程安全
同步容器是Java早期提供的线程安全集合方案,核心思路是通过 synchronized 关键字对集合的所有方法加锁,保证同一时间只有一个线程能访问集合方法,从而避免并发问题。
Vector和Hashtable
Vector是List接口的同步实现,Hashtable是Map接口的同步实现,这两个类在早期JDK中就已经存在,它们的所有公开方法都用 synchronized 修饰,比如Vector的add方法:
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}Hashtable的put方法同样如此:
public synchronized V put(K key, V value) {
// 检查key是否为null
if (value == null) {
throw new NullPointerException();
}
// 省略哈希计算、存入逻辑
return null;
}这种实现方式的优点是使用简单,不需要额外操作就能保证线程安全。但缺点也非常明显:所有方法都加全局锁,同一时间只能有一个线程访问集合,并发性能极差,在高并发场景下会成为性能瓶颈,因此现在实际开发中已经很少使用这两个类。
Collections工具类的同步包装方法
JDK的Collections工具类提供了一系列同步包装方法,可以把非线程安全的集合包装成线程安全的集合,比如 synchronizedList、synchronizedMap、synchronizedSet 等。
使用示例:
import java.util.*;
public class SynchronizedCollectionDemo {
public static void main(String[] args) {
// 包装ArrayList为线程安全的List
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
// 包装HashMap为线程安全的Map
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
// 包装HashSet为线程安全的Set
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
// 多线程写入测试
for (int i = 0; i < 10; i++) {
final int index = i;
new Thread(() -> {
syncList.add("element" + index);
syncMap.put("key" + index, index);
syncSet.add("setElement" + index);
}).start();
}
}
}需要注意的是,虽然这些包装后的集合本身是线程安全的,但如果是遍历操作,仍然需要手动加锁,否则还是可能出现并发修改异常。比如遍历syncList时:
// 正确的遍历方式,需要手动加锁
synchronized (syncList) {
Iterator<String> it = syncList.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}同步包装类的实现原理和Vector、Hashtable类似,也是通过 synchronized 关键字对方法或者代码块加锁,只不过它们是把原集合作为内部成员变量,所有操作都转发给原集合,同时加锁保证安全,性能问题和早期的同步容器一样,高并发场景下表现不佳。
使用并发容器保证线程安全
为了解决同步容器性能差的问题,JDK 1.5之后在java.util.concurrent包下提供了一系列并发容器,这些容器针对多线程场景做了优化,在保证线程安全的同时,大幅提升了并发性能。

ConcurrentHashMap
ConcurrentHashMap是HashMap的线程安全替代方案,在JDK 1.7和JDK 1.8中的实现原理有所不同:JDK 1.7采用分段锁(Segment)的机制,每个Segment对应一个小的哈希表,锁的粒度是Segment,不同Segment的操作可以并发执行;JDK 1.8之后放弃了分段锁,采用CAS + synchronized 的方式实现,锁的粒度进一步细化到哈希表的每个桶(bucket),并发性能更高。
使用示例:
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapDemo {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
// 多线程写入
for (int i = 0; i < 10; i++) {
final int index = i;
new Thread(() -> {
concurrentMap.put("key" + index, index);
}).start();
}
// 不需要额外加锁就可以安全遍历
concurrentMap.forEach((key, value) -> {
System.out.println(key + ":" + value);
});
}
}ConcurrentHashMap的遍历是弱一致性的,不会抛出并发修改异常,遍历的是集合在某个时刻的快照或者实时状态,具体取决于遍历的方式,适合高并发场景下的Map使用。
CopyOnWriteArrayList和CopyOnWriteArraySet
CopyOnWrite容器也叫写时复制容器,核心思想是:当往容器中添加元素时,不直接修改原容器,而是先将原容器复制一份,在复制的容器上进行修改,修改完成后再将原容器的引用指向新的容器。读操作不需要加锁,因为读的是原容器,写操作会加锁,避免多个线程同时复制容器。
CopyOnWriteArrayList的使用示例:
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteDemo {
public static void main(String[] args) {
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
// 写线程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
cowList.add("write" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 读线程,不需要加锁,也不会抛异常
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("当前列表大小:" + cowList.size());
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}这种容器的优点是读性能极高,因为读操作完全不需要加锁,适合读多写少的场景。缺点是写操作开销大,因为每次写都要复制整个容器,而且会占用更多的内存,因为旧容器可能还在被读线程使用,无法及时回收。
其他并发容器
除了上述两种常用的并发容器,java.util.concurrent包还提供了很多其他场景的线程安全集合:
ConcurrentSkipListMap:线程安全的有序Map,基于跳表实现,支持排序,性能比同步的TreeMap高很多ConcurrentSkipListSet:线程安全的有序Set,基于ConcurrentSkipListMap实现ArrayBlockingQueue:基于数组实现的有界阻塞队列,适合生产者消费者模式LinkedBlockingQueue:基于链表实现的可选有界阻塞队列,吞吐量通常高于ArrayBlockingQueuePriorityBlockingQueue:支持优先级的无界阻塞队列
不同方案的对比和选择
为了帮助开发者选择合适的线程安全集合方案,下面从性能、适用场景等维度做对比:
| 方案类型 | 代表实现 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 同步容器 | Vector、Hashtable、Collections包装类 | 使用简单,理解成本低 | 全局锁,并发性能差 | 并发量极低的场景,几乎没有性能要求的情况 |
| 并发容器-哈希类 | ConcurrentHashMap | 并发性能高,支持高并发读写 | 遍历弱一致性,部分操作不支持(比如空键值) | 高并发场景下的Map存储,比如缓存、共享配置等 |
| 并发容器-写时复制类 | CopyOnWriteArrayList、CopyOnWriteArraySet | 读性能极高,无锁读 | 写开销大,内存占用高 | 读多写少,比如配置列表、黑白名单等场景 |
| 并发容器-阻塞队列 | ArrayBlockingQueue、LinkedBlockingQueue | 支持阻塞操作,天然适配生产者消费者模式 | 主要用于队列场景,不适合普通集合存储 | 线程池任务队列、消息传递、生产者消费者场景 |
使用注意事项
即使使用了线程安全的集合,在实际开发中也有不少需要注意的点,避免踩坑:
复合操作需要额外加锁
很多并发容器的单个操作是线程安全的,但复合操作(比如先检查再更新、先获取再修改)不是线程安全的。比如ConcurrentHashMap的putIfAbsent方法是原子操作,但如果我们需要实现如果存在就更新,不存在就插入的逻辑,就需要使用容器提供的原子复合方法,而不是自己写判断逻辑。
错误示例:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 错误的复合操作,不是线程安全的
if (!map.containsKey("key")) {
map.put("key", 1);
}正确示例,使用原子方法:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 使用putIfAbsent原子方法,保证线程安全
map.putIfAbsent("key", 1);避免大对象写入CopyOnWrite容器
CopyOnWrite容器每次写都会复制整个底层数组,如果集合中的元素很大,或者集合本身容量很大,写操作会占用大量内存和CPU资源,甚至导致内存溢出,因此这类容器不适合存储大对象或者容量过大的集合。
根据业务场景选择容器
不要盲目选择并发容器,比如如果集合只在单线程中使用,就不需要用线程安全的集合,避免不必要的性能开销。如果读写频率差不多,也不适合用CopyOnWrite容器,这时候ConcurrentHashMap或者同步容器可能更合适。
总结
Java集合框架在多线程下的线程安全保证有多种方案,同步容器是最早期的方案,实现简单但性能差,适合极低并发场景;并发容器是现在的主流选择,其中ConcurrentHashMap适合高并发的Map场景,CopyOnWriteArrayList适合读多写少的List场景,阻塞队列适合生产者消费者场景。开发者需要根据具体的业务场景、并发量、读写比例来选择合适的集合实现,同时注意复合操作的线程安全,避免因为使用不当导致的问题。
Java集合框架多线程安全同步容器并发容器CopyOnWrite修改时间:2026-05-24 20:31:35