1. 概述

Java 的 集合框架 是语言的核心组件之一,提供了丰富的接口和实现类,支持我们以直观的方式创建和操作各种集合。

在单线程环境下使用普通集合非常简单,但在多线程(并发)环境中,直接使用非同步集合可能会导致数据不一致、竞态条件等严重问题。

为此,Java 提供了强大的支持:通过 Collections 类中的多个静态方法,我们可以为已有集合创建线程安全的同步包装器(synchronization wrappers)。

这些包装器能够快速地将任意集合转换为线程安全版本,适用于大多数并发场景。

在本文中,我们将深入探讨这些静态同步包装器,并对比同步集合与并发集合之间的区别。

2. synchronizedCollection() 方法

我们首先介绍的是 synchronizedCollection() 方法。顾名思义,它返回一个由指定 Collection 支持的线程安全集合。

来看一个简单的示例:

Collection<Integer> syncCollection = Collections.synchronizedCollection(new ArrayList<>());
Runnable listOperations = () -> {
    syncCollection.addAll(Arrays.asList(1, 2, 3, 4, 5, 6));
};

Thread thread1 = new Thread(listOperations);
Thread thread2 = new Thread(listOperations);
thread1.start();
thread2.start();
thread1.join();
thread2.join();

assertThat(syncCollection.size()).isEqualTo(12);

✅ 创建一个同步集合就是这么简单粗暴。

上面代码中,我们启动了两个线程同时向集合中添加元素。由于使用了同步包装器,最终集合大小为 12,说明线程安全得到了保障。

⚠️ 注意:虽然集合本身是线程安全的,但迭代操作仍需手动加锁。

3. synchronizedList() 方法

类似地,我们可以使用 synchronizedList() 方法来创建一个线程安全的 List 实例。

List<Integer> syncList = Collections.synchronizedList(new ArrayList<>());

这个方法的用法和 synchronizedCollection() 几乎一模一样。

但如果我们要对同步 List 进行迭代操作,就必须显式使用 synchronized 块来确保操作的原子性:

List<String> syncCollection = Collections.synchronizedList(Arrays.asList("a", "b", "c"));
List<String> uppercasedCollection = new ArrayList<>();

Runnable listOperations = () -> {
    synchronized (syncCollection) {
        syncCollection.forEach((e) -> {
            uppercasedCollection.add(e.toUpperCase());
        });
    }
};

⚠️ 踩坑提醒:对同步集合进行迭代时,必须加锁,否则会抛出 ConcurrentModificationException

4. synchronizedMap() 方法

Collections 类还提供了 synchronizedMap() 方法,用于创建线程安全的 Map 实例:

Map<Integer, String> syncMap = Collections.synchronizedMap(new HashMap<>());

✅ 使用方式和 List/Set 类似,同样适用于多线程环境下的 Map 操作。

5. synchronizedSortedMap() 方法

如果需要线程安全的排序 Map,可以使用 synchronizedSortedMap() 方法,它返回一个由 SortedMap 实现支持的同步视图:

Map<Integer, String> syncSortedMap = Collections.synchronizedSortedMap(new TreeMap<>());

6. synchronizedSet() 方法

同样的,使用 synchronizedSet() 方法可以轻松创建线程安全的 Set

Set<Integer> syncSet = Collections.synchronizedSet(new HashSet<>());

7. synchronizedSortedSet() 方法

最后是 synchronizedSortedSet() 方法,用于创建线程安全的排序 Set:

SortedSet<Integer> syncSortedSet = Collections.synchronizedSortedSet(new TreeSet<>());

8. 同步集合 vs 并发集合

前面我们介绍了 Collections 提供的同步包装器,现在我们来聊聊它们与 Java 并发包中的并发集合(如 ConcurrentHashMapBlockingQueue)有什么不同。

8.1. 同步集合

同步集合通过内置锁(intrinsic locking)实现线程安全。即每个方法都通过 synchronized 关键字加锁,整个集合对象被锁定。

✅ 优点:保证了数据一致性。

❌ 缺点:性能较差,同一时间只能有一个线程访问集合(串行化访问)。

8.2. 并发集合

并发集合(如 ConcurrentHashMap)通过将数据结构分段(segmentation)来实现线程安全。

✅ 优点:多个线程可以同时访问不同段,性能更高。

❌ 缺点:不提供强一致性语义,但在大多数场景下已经足够。

💡 建议:对于高并发读写场景,优先选择并发集合;如果并发度不高,同步集合也完全够用。

9. 总结

本文深入讲解了 Java Collections 类中提供的同步集合包装器,包括 synchronizedListsynchronizedMap 等方法的使用方式。

同时,我们也对比了同步集合与并发集合在实现机制和性能表现上的差异,帮助你在实际开发中做出更合适的选择。

一如既往,本文所有示例代码均可在 GitHub 上找到。


原始标题:An Introduction to Synchronized Java Collections | Baeldung