1.本篇前言
Java Collections.synchronizedMap() 和 ConcurrentHashMap提供线程安全的 Map 实现以在并发应用程序中使用。在本教程中,我们将重点关注Collections.synchronizedMap()和ConcurrentHashMap之间的核心区别。
2 Collections.synchronizedMap()和ConcurrentHashMap 解析
ConcurrentHashMap 继承AbstractMap类并实现了ConcurrentMap接口,并在Java 1.5 版本中引入。HashMap类是非同步的,当多个线程同时使用它时会导致数据不一致。ConcurrentHashMap是HashMap类的线程安全替代方案。
public class ConcurrentHashMap <K, V> extends AbstractMap<K, V> implements Concurrent Map<K, V>, Serializable
synchronizedMap()是Collections类中的一个方法,在Java 1.2 版中引入。它会将我们在参数中提供的指定Map返回成一个同步Map。
public static Map<K, V> synchronizedMap(Map<K, V> map)
2.1 Collections.synchronizedMap()和ConcurrentHashMap的区别
2.1.1 锁和并发
在内部。 ConcurrentHashMap 使用桶的内部分段。它将所有键值对存储在这些桶中。默认情况下,它维护 16 个桶。
ConcurrentHashMap 允许对所有线程进行非阻塞读取访问,但运行的线程必须在将数据写入特定存储桶之前获得对特定存储桶的锁定。它不需要锁定整个映射对象,因此多个线程也可以同时写入。
使用 synchronizedMap() 创建的同步Map允许对其进行串行访问,因此一次只有一个线程可以访问该Map。
2.1.2 Null Keys and Values
ConcurrentHashMap内部使用HashTable 作为底层数据结构;因此,它不允许null作为键或值。
synchronizedMap(),null支持由支持传入的Map参数决定。如果是HashMap或LinkedHashMap ,那么我们可以在Map中插入一个空键和任意数量的空值。但如果是TreeMap,那么我们不能插入空键,但我们仍然可以有任意数量的空值。
// ConcurrentHashMap中放入空值 空键
ConcurrentHashMap<String, String> chmap = new ConcurrentHashMap<>();
chmap.put(null, "value"); // NullPointerException
chmap.put("key", null); // NullPointerException
// SynchronizedMap中放入空值 空键
Map<String, Integer> hmap = Collections.synchronizedMap(new HashMap<String, Integer>());
hmap.put(null, 1); // 无异常
Map<String, Integer> tmap = Collections.synchronizedMap(new TreeMap<String, Integer>());
tmap.put(null, 1); // NullPointerException
2.1.3 ConcurrentModificationException异常
ConcurrentHashMap 不会抛出 ConcurrentModificationException,因此我们在迭代时修改数据并不会抛出此异常。 ConcurrentHashMap 的迭代器是安全失败(fail-safe)的,因为它不会引发 ConcurrentModificationException。
从 synchronizedMap() 获得的迭代器是快速失败(fail—fast)的,会引发 ConcurrentModificationException。
2.1.4 元素排序
ConcurrentHashMap不维护其元素的任何顺序,即它不会保证首先插入到Map中的元素将在Map迭代时首先打印。
Collections.synchronizedMap()由指定的Map支持并保留其顺序。如果我们将HashMap传递给它,则不会保留顺序,但如果我们传递TreeMap ,它会保留TreeMap中元素的顺序。
2.1.5 性能
在ConcurrentHashMap实例上,多个线程可以同时操作,以便以安全的方式执行并发读写操作,因此ConcurrentHashMap的性能优于其他同步映射。
Collections.synchronizedMap()提供串行访问,即只允许一个线程对其进行读取或写入操作。这增加了其他线程的等待时间,相对降低了map的性能。
我们可以使用Java Microbenchmark Harness来比较这两者的性能。
/**
* 功能描述:
* @author TuYong
* @date 2022/9/8 14:16
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(value = 1, warmups = 1)
public class BenchmarkRunner {
/**
*
* @param args
* @throws RunnerException
*/
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(BenchmarkRunner.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
/**
* synchronizedMap
*/
@Benchmark
public void synchronizedMapReadAndWrite() {
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());
readAndWriteTest(map);
}
/**
* ConcurrentHashMap
*/
@Benchmark
public void concurrentHashMapReadAndWrite() {
Map<String, Integer> map = new ConcurrentHashMap<>();
readAndWriteTest(map);
}
/**
* 放入和获取1000个元素的测试方法
* @param map
*/
private void readAndWriteTest(final Map<String, Integer> map) {
for (int i = 0; i < 1000; i++) {
Integer randomNumber = (int) Math.ceil(Math.random() * 1000);
map.get(String.valueOf(randomNumber));
map.put(String.valueOf(randomNumber), randomNumber);
}
}
}
从结果可以看出来ConcurrentHashMap比Collections.synchronizedMap()执行得更好。
2.1.6 何时使用
当数据一致性至关重要时,我们应该选择 Collections.synchronizedMap(),对于写入操作远多于读取操作的性能关键型应用程序,我们应该选择ConcurrentHashMap 。
这是因为Collections.synchronizedMap()要求每个线程都为读/写操作获取整个对象的锁。相比之下,ConcurrentHashMap允许线程在集合的不同段上获取锁,并同时进行修改。