ConcurrentHashMap和Collections.synchronizedMap(Map)有什么区别?

我有一个地图是由多个线程同时修改。

Java API中似乎有三种不同的同步Map实现:

  • Hashtable
  • Collections.synchronizedMap(Map)
  • ConcurrentHashMap

据我所知, Hashtable是一个旧的实现(扩展了过时的Dictionary类),后来为适应Map接口而进行了修改。 虽然它同步的,但它似乎有严重的可扩展性问题 ,并且不鼓励新项目。

但是另外两个呢? Collections.synchronizedMap(Map)ConcurrentHashMap s返回的Maps之间有什么区别? 哪一个适合哪种情况?

为了您的需要,使用ConcurrentHashMap 。 它允许从几个线程同时修改Map,而不需要阻塞它们。 Collections.synchronizedMap(map)会创建一个阻塞映射,这会降低性能,尽管确保一致性(如果使用得当)。

如果需要确保数据一致性,则使用第二个选项,并且每个线程都需要具有最新的地图视图。 如果性能至关重要,则使用第一个线程,并且每个线程只将数据插入到地图中,读取操作的频率较低。

 ╔═══════════════╦═══════════════════╦═══════════════════╦═════════════════════╗ ║ Property ║ HashMap ║ Hashtable ║ ConcurrentHashMap ║ ╠═══════════════╬═══════════════════╬═══════════════════╩═════════════════════╣ ║ Null ║ allowed ║ not allowed ║ ║ values/keys ║ ║ ║ ╠═══════════════╬═══════════════════╬═════════════════════════════════════════╣ ║Is thread-safe ║ no ║ yes ║ ╠═══════════════╬═══════════════════╬═══════════════════╦═════════════════════╣ ║ Lock ║ not ║ locks the whole ║ locks the portion ║ ║ mechanism ║ applicable ║ map ║ ║ ╠═══════════════╬═══════════════════╩═══════════════════╬═════════════════════╣ ║ Iterator ║ fail-fast ║ fail-safe ║ ╚═══════════════╩═══════════════════════════════════════╩═════════════════════╝ 

关于锁定机制: Hashtable 锁定对象 ,而ConcurrentHashMap 只锁定存储桶 。

对于Hashtable的“可伸缩性问题”在Collections.synchronizedMap(Map)以完全相同的方式呈现 – 它们使用非常简单的同步,这意味着只有一个线程可以同时访问地图。

当你有简单的插入和查找(除非你做得非常精细),这不是什么大问题,但是当你需要迭代整个Map,这会花费很长时间来处理一个大的Map,一个线程做到这一点,所有其他人不得不等待,如果他们想插入或查找任何东西。

ConcurrentHashMap使用非常复杂的技术来减少同步的需要,并允许多线程并行读取,而无需同步,更重要的是,提供了一个不需要同步的Iterator ,甚至允许在Iterator过程中修改映射(尽管它不作任何保证是否会返回在迭代过程中插入的元素)。

当你可以使用ConcurrentHashMap时,首选的是ConcurrentHashMap – 尽管它至少需要Java 5。

它被设计成在被多个线程使用时很好地扩展。 当一次只有一个线程访问Map时,性能可能稍微差一些,但是当多个线程同时访问地图时性能会更好。

我发现了一篇博客文章 ,它重现了我完全推荐的优秀书籍Java Concurrency In Practice中的表格。

Collections.synchronizedMap只有在需要包含具有其他特征的地图时才有意义,也许某种有序的地图,比如TreeMap。

这两者之间的主要区别在于, ConcurrentHashMap将只锁定正在更新的数据的一部分,而其他部分的数据可以被其他线程访问。 但是, Collections.synchronizedMap()将在更新时锁定所有数据,其他线程只能在释放锁时访问数据。 如果有很多更新操作和相对较少的读取操作,则应该选择ConcurrentHashMap

另一个区别是, ConcurrentHashMap不会保留传入的Map中的元素的顺序,它在存储数据时与HashMap类似。 不保证元素顺序被保留。 而Collections.synchronizedMap()将保留传入的Map的元素顺序。例如,如果将TreeMap传递给ConcurrentHashMap ,则ConcurrentHashMap中的元素顺序可能与TreeMap的顺序不同,但是Collections.synchronizedMap()将保存顺序。

此外, ConcurrentHashMap可以保证当一个线程正在更新地图时,不会抛出ConcurrentModificationException ,另一个线程正在遍历从地图获取的迭代器。 但是, Collections.synchronizedMap()不能保证这一点。

有一篇文章演示了这两者的不同之处,以及ConcurrentSkipListMap

ConcurrentHashMap ,锁被应用于一个段而不是整个Map。 每个段管理自己的内部哈希表。 该锁只适用于更新操作。 Collections.synchronizedMap(Map)同步整个地图。

  • HashtableConcurrentHashMap不允许null键或null值。

  • Collections.synchronizedMap(Map)同步所有操作( getputsize等)。

  • ConcurrentHashMap支持完全并发的检索,并可调整预期的并发更新。

像往常一样,有并发 – 开销 – 速度的折衷涉及。 您真的需要考虑应用程序的详细并发性需求来做出决定,然后测试您的代码以查看是否足够好。

你对HashTable正确,你可以忘记它。

你的文章提到这样一个事实,即虽然HashTable和synchronized包装类通过一次只允许一个线程访问map来提供基本的线程安全性,但这并不是真正的线程安全性,因为许多复合操作仍然需要额外的同步,例:

 synchronized (records) { Record rec = records.get(id); if (rec == null) { rec = new Record(id); records.put(id, rec); } return rec; } 

但是,不要认为ConcurrentHashMap是一个典型的带有典型synchronized块的HashMap替代方案,如上所示。 阅读这篇文章,以更好地了解它的错综复杂。

这里有几个:

1)ConcurrentHashMap只锁定Map的一部分,但SynchronizedMap锁定整个MAp。
2)ConcurrentHashMap比SynchronizedMap具有更好的性能和更多的可扩展性。
3)在多个阅读器和单写入器的情况下,ConcurrentHashMap是最好的选择。

本文来自Java中的ConcurrentHashMap和散列表的区别

的ConcurrentHashMap

  • 当您的项目需要非常高的并发性时,您应该使用ConcurrentHashMap。
  • 这是线程安全的,没有同步整个地图。
  • 写入完成后,读取速度可能会非常快。
  • 在对象级别没有锁定。
  • 在hashmap桶级别,锁定的粒度更加精细。
  • 如果一个线程试图修改而另一个线程正在迭代,则ConcurrentHashMap不会抛出ConcurrentModificationException异常。
  • ConcurrentHashMap使用大量的锁。

SynchronizedHashMap

  • 在对象级别同步。
  • 每个读/写操作都需要获取锁定。
  • 锁定整个集合是一个性能开销。
  • 这基本上只允许一个线程访问整个地图,并阻止所有其他线程。
  • 这可能会引起争议。
  • SynchronizedHashMap返回Iterator,它在并发修改时快速失败。

资源

ConcurrentHashMap针对并发访问进行了优化。

访问不锁定整个地图,但使用更细粒度的策略,这可以提高可伸缩性。 还有专门用于并发访问的函数enhanvements,例如并发迭代器。

我们可以通过使用ConcurrentHashMap和synchronizedHashmap和Hashtable来实现线程安全。 但是,如果你看看他们的架构,那么就会有很多不同。

  1. 同步的Hashmap和Hashtable

两者都将锁定在对象级别。 所以如果你想执行任何操作,如put / get,那么你必须先获得锁。 同时,其他线程不允许执行任何操作。 所以一次只有一个线程可以对此进行操作。 所以等候时间会增加 与ConcurrentHashMap比较,可以说性能相对较低。

  1. 的ConcurrentHashMap

它将维持段级的锁定。 它有16个段,默认情况下保持并发级别为16。 所以一次有16个线程可以在ConcurrentHashMap上运行。 而且,读取操作不需要锁定。 所以任何数量的线程都可以对它执行get操作。

如果线程1想要在段2中执行put操作,并且线程2想要在段4上执行put操作,那么在这里允许。 意思是说,16个线程一次可以对ConcurrentHashMap执行更新(put / delete)操作。

所以这里的等待时间会少一些。 因此,性能比同步的Hashmap和Hashtable要好。

ConcurrentHashMap除了它提供的并发功能之外,还有一个关键的特性 ,那就是故障安全迭代器。 我已经看到开发人员使用ConcurrentHashMap只是因为他们想编辑入口集 – 放置/删除而迭代它。 Collections.synchronizedMap(Map)不提供失效安全的迭代器,但它提供了失效快速迭代器。 快速迭代器使用迭代期间无法编辑的映射大小的快照。

  1. 如果数据一致性非常重要 – 使用Hashtable或Collections.synchronizedMap(Map)。
  2. 如果速度/性能非常重要,数据更新可能受到影响 – 请使用ConcurrentHashMap。

Collections.synchronizedMap()方法同步HashMap的所有方法,并有效地将其减少到一个线程一次可以进入的数据结构,因为它锁定了通用锁上的每个方法。

在ConcurrentHashMap中,同步的做法稍有不同。 ConcurrentHashMap不是将每个方法都锁定在一个通用的锁上,而是对单独的存储区使用单独的锁,因此只锁定了一部分Map。 默认情况下,有16个桶,单独的桶也是独立的。 所以默认的并发级别是16.这意味着在理论上任何给定的时间16个线程可以访问ConcurrentHashMap,如果他们都要分开桶。

一般来说,如果您想使用ConcurrentHashMap确保您准备好错过“更新”
(即打印HashMap的内容并不能保证打印最新的Map),并使用CyclicBarrier API来保证程序生命周期的一致性。

除了已经建议,我想发布与SynchronizedMap相关的源代码。

为了使Map线程安全,我们可以使用Collections.synchronizedMap语句并输入地图实例作为参数。

CollectionssynchronizedMap的实现如下所示

  public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) { return new SynchronizedMap<>(m); } 

如您所见,输入的Map对象由SynchronizedMap对象封装。
我们来深入一下SynchronizedMap的实现,

  private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable { private static final long serialVersionUID = 1978198479659022715L; private final Map<K,V> m; // Backing Map final Object mutex; // Object on which to synchronize SynchronizedMap(Map<K,V> m) { this.m = Objects.requireNonNull(m); mutex = this; } SynchronizedMap(Map<K,V> m, Object mutex) { this.m = m; this.mutex = mutex; } public int size() { synchronized (mutex) {return m.size();} } public boolean isEmpty() { synchronized (mutex) {return m.isEmpty();} } public boolean containsKey(Object key) { synchronized (mutex) {return m.containsKey(key);} } public boolean containsValue(Object value) { synchronized (mutex) {return m.containsValue(value);} } public V get(Object key) { synchronized (mutex) {return m.get(key);} } public V put(K key, V value) { synchronized (mutex) {return m.put(key, value);} } public V remove(Object key) { synchronized (mutex) {return m.remove(key);} } public void putAll(Map<? extends K, ? extends V> map) { synchronized (mutex) {m.putAll(map);} } public void clear() { synchronized (mutex) {m.clear();} } private transient Set<K> keySet; private transient Set<Map.Entry<K,V>> entrySet; private transient Collection<V> values; public Set<K> keySet() { synchronized (mutex) { if (keySet==null) keySet = new SynchronizedSet<>(m.keySet(), mutex); return keySet; } } public Set<Map.Entry<K,V>> entrySet() { synchronized (mutex) { if (entrySet==null) entrySet = new SynchronizedSet<>(m.entrySet(), mutex); return entrySet; } } public Collection<V> values() { synchronized (mutex) { if (values==null) values = new SynchronizedCollection<>(m.values(), mutex); return values; } } public boolean equals(Object o) { if (this == o) return true; synchronized (mutex) {return m.equals(o);} } public int hashCode() { synchronized (mutex) {return m.hashCode();} } public String toString() { synchronized (mutex) {return m.toString();} } // Override default methods in Map @Override public V getOrDefault(Object k, V defaultValue) { synchronized (mutex) {return m.getOrDefault(k, defaultValue);} } @Override public void forEach(BiConsumer<? super K, ? super V> action) { synchronized (mutex) {m.forEach(action);} } @Override public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) { synchronized (mutex) {m.replaceAll(function);} } @Override public V putIfAbsent(K key, V value) { synchronized (mutex) {return m.putIfAbsent(key, value);} } @Override public boolean remove(Object key, Object value) { synchronized (mutex) {return m.remove(key, value);} } @Override public boolean replace(K key, V oldValue, V newValue) { synchronized (mutex) {return m.replace(key, oldValue, newValue);} } @Override public V replace(K key, V value) { synchronized (mutex) {return m.replace(key, value);} } @Override public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) { synchronized (mutex) {return m.computeIfAbsent(key, mappingFunction);} } @Override public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) { synchronized (mutex) {return m.computeIfPresent(key, remappingFunction);} } @Override public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) { synchronized (mutex) {return m.compute(key, remappingFunction);} } @Override public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) { synchronized (mutex) {return m.merge(key, value, remappingFunction);} } private void writeObject(ObjectOutputStream s) throws IOException { synchronized (mutex) {s.defaultWriteObject();} } } 

SynchronizedMap功能可以概括为向输入的Map对象的主要方法添加一个锁。 所有被锁保护的方法都不能被多个线程同时访问。 这意味着像putget这样的普通操作可以由一个线程同时为Map对象中的所有数据执行。

它使Map对象线程安全,但性能可能成为一些问题。

ConcurrentMap在实现上要复杂得多,我们可以参考构建一个更好的HashMap的细节。 简而言之,它的实现既考虑到了线程的安全性,又考虑到了性能。