我有一个 Map 将由多个线程同时修改。
Java API 中似乎存在三种不同的同步 Map 实现:
哈希表
Collections.synchronizedMap(地图)
并发哈希映射
据我了解,Hashtable
是一个旧实现(扩展过时的 Dictionary
类),后来经过调整以适应 Map
接口。虽然它是同步的,但它似乎有严重的scalability issues,并且不鼓励用于新项目。
但是另外两个呢? Collections.synchronizedMap(Map)
和 ConcurrentHashMap
返回的地图有什么区别?哪一种适合哪一种情况?
根据您的需要,请使用 ConcurrentHashMap
。它允许从多个线程同时修改 Map 而无需阻塞它们。 Collections.synchronizedMap(map)
创建一个会降低性能的阻塞 Map,但可以确保一致性(如果使用得当)。
如果您需要确保数据一致性,请使用第二个选项,并且每个线程都需要拥有最新的地图视图。如果性能很关键,则使用第一个,并且每个线程只将数据插入到映射中,读取发生的频率较低。
╔═══════════════╦═══════════════════╦═══════════════════╦═════════════════════╗
║ Property ║ HashMap ║ Hashtable ║ ConcurrentHashMap ║
╠═══════════════╬═══════════════════╬═══════════════════╩═════════════════════╣
║ Null ║ allowed ║ not allowed ║
║ values/keys ║ ║ ║
╠═══════════════╬═══════════════════╬═════════════════════════════════════════╣
║ Thread-safety ║ ║ ║
║ features ║ no ║ yes ║
╠═══════════════╬═══════════════════╬═══════════════════╦═════════════════════╣
║ Lock ║ not ║ locks the whole ║ locks the portion ║
║ mechanism ║ applicable ║ map ║ ║
╠═══════════════╬═══════════════════╩═══════════════════╬═════════════════════╣
║ Iterator ║ fail-fast ║ weakly consistent ║
╚═══════════════╩═══════════════════════════════════════╩═════════════════════╝
关于锁定机制:Hashtable
locks the object,而 ConcurrentHashMap
锁定 only the bucket。
Hashtable
不是地图的锁定部分。看看实现。它使用 synchronized
键,没有提供锁,因此基本上意味着它在每个操作中锁定整个 hashtable
。
Hashtable
的“可扩展性问题”在 Collections.synchronizedMap(Map)
中以完全相同的方式存在 - 它们使用非常简单的同步,这意味着只有一个线程可以同时访问映射。
当您进行简单的插入和查找时,这不是什么大问题(除非您非常密集地执行此操作),但是当您需要遍历整个 Map 时,这会成为一个大问题,这对于大型 Map 可能需要很长时间 - 而一个线程执行此操作,所有其他线程如果要插入或查找任何内容,则必须等待。
ConcurrentHashMap
使用非常复杂的技术来减少同步需求,并允许多个线程在不同步的情况下进行并行读取访问,更重要的是,它提供了一个不需要同步的 Iterator
,甚至允许在交互期间修改 Map(尽管它不保证在迭代期间插入的元素是否会被返回)。
这两者之间的主要区别在于 ConcurrentHashMap
将仅锁定正在更新的部分数据,而其他部分数据可以被其他线程访问。但是,Collections.synchronizedMap()
会在更新时锁定所有数据,其他线程只有在锁定释放后才能访问数据。如果更新操作较多而读取操作相对较少,则应选择ConcurrentHashMap
。
还有一个区别是 ConcurrentHashMap
不会保留传入的 Map 中元素的顺序。它在存储数据时类似于 HashMap
。不能保证保留元素顺序。虽然 Collections.synchronizedMap()
将保留传入 Map 的元素顺序。例如,如果您将 TreeMap
传递给 ConcurrentHashMap
,则 ConcurrentHashMap
中的元素顺序可能与 TreeMap
中的顺序不同},但 Collections.synchronizedMap()
将保留顺序。
此外,ConcurrentHashMap
可以保证在一个线程正在更新映射并且另一个线程正在遍历从映射获取的迭代器时不会抛出 ConcurrentModificationException
。但是,Collections.synchronizedMap()
不能保证这一点。
有 one post 证明了这两者的区别,还有 ConcurrentSkipListMap
。
当您可以使用 ConcurrentHashMap 时,它是首选 - 尽管它至少需要 Java 5。
它被设计为在被多个线程使用时可以很好地扩展。当一次只有一个线程访问 Map 时,性能可能会稍微差一些,但当多个线程同时访问 Map 时,性能会好很多。
我找到了一个 blog entry,它复制了优秀书籍 Java Concurrency In Practice 中的一个表格,我强烈推荐这本书。
Collections.synchronizedMap 仅在您需要包装具有其他一些特征的地图时才有意义,也许是某种有序地图,例如 TreeMap。
同步地图:
Synchronized Map 也与 Hashtable 没有太大区别,并且在并发 Java 程序中提供了类似的性能。 Hashtable 和 SynchronizedMap 之间的唯一区别是 SynchronizedMap 不是遗留的,您可以使用 Collections.synchronizedMap() 方法包装任何 Map 以创建它的同步版本。
并发哈希映射:
ConcurrentHashMap 类提供标准 HashMap 的并发版本。这是对 Collections 类中提供的 synchronizedMap 功能的改进。
与 Hashtable 和 Synchronized Map 不同,它从不锁定整个 Map,而是将 Map 划分为段并在这些段上完成锁定。如果读取器线程的数量大于写入器线程的数量,它的性能会更好。
ConcurrentHashMap 默认分为 16 个区域并应用锁。可以在初始化 ConcurrentHashMap 实例时设置此默认数字。在特定段中设置数据时,将获得该段的锁定。这意味着如果两个更新都影响单独的存储桶,它们仍然可以安全地同时执行,从而最大限度地减少锁争用,从而最大限度地提高性能。
ConcurrentHashMap 不会抛出 ConcurrentModificationException
如果一个线程尝试修改它而另一个线程正在对其进行迭代,则 ConcurrentHashMap 不会抛出 ConcurrentModificationException
synchornizedMap 和 ConcurrentHashMap 的区别
Collections.synchornizedMap(HashMap) 将返回一个几乎等同于 Hashtable 的集合,其中对 Map 的每个修改操作都锁定在 Map 对象上,而在 ConcurrentHashMap 的情况下,通过根据并发级别将整个 Map 划分为不同的分区来实现线程安全并且只锁定特定部分而不是锁定整个地图。
ConcurrentHashMap 不允许空键或空值,而同步 HashMap 允许一个空键。
类似链接
在 ConcurrentHashMap
中,锁定应用于段而不是整个地图。每个段管理自己的内部哈希表。该锁仅适用于更新操作。 Collections.synchronizedMap(Map)
同步整个地图。
Hashtable 和 ConcurrentHashMap 不允许空键或空值。
Collections.synchronizedMap(Map) 同步所有操作(get、put、size 等)。
ConcurrentHashMap 支持检索的完全并发,以及可调整的更新预期并发。
像往常一样,涉及到并发——开销——速度的权衡。您确实需要考虑应用程序的详细并发要求才能做出决定,然后测试您的代码以查看它是否足够好。
您对 HashTable
的看法是正确的,您可以忘记它。
Your article 提到了这样一个事实,虽然 HashTable 和同步包装类通过一次只允许一个线程访问映射来提供基本的线程安全,但这不是“真正的”线程安全,因为许多复合操作仍然需要额外的同步,例如:
synchronized (records) {
Record rec = records.get(id);
if (rec == null) {
rec = new Record(id);
records.put(id, rec);
}
return rec;
}
但是,不要认为 ConcurrentHashMap
是具有典型 synchronized
块的 HashMap
的简单替代方案,如上所示。阅读 this 文章以更好地了解其复杂性。
这里有几个:
1) ConcurrentHashMap 只锁定 Map 的一部分,而 SynchronizedMap 锁定整个 MAp。 2) ConcurrentHashMap 比 SynchronizedMap 性能更好,可扩展性更强。 3)如果是多个阅读器和单个作者 ConcurrentHashMap 是最好的选择。
此文字来自 Difference between ConcurrentHashMap and hashtable in Java
我们可以通过使用 ConcurrentHashMap 和 synchronisedHashmap 和 Hashtable 来实现线程安全。但是如果你看看他们的架构,就会有很大的不同。
同步Hashmap和Hashtable
两者都将在对象级别保持锁定。因此,如果您想执行 put/get 之类的任何操作,则必须先获取锁。同时,不允许其他线程执行任何操作。因此,一次只能有一个线程对此进行操作。所以这里的等待时间会增加。与 ConcurrentHashMap 相比,我们可以说性能相对较低。
并发哈希映射
它将保持段级别的锁定。它有 16 个段,默认保持并发级别为 16。所以一次可以有 16 个线程对 ConcurrentHashMap 进行操作。此外,读操作不需要锁。所以任意数量的线程都可以对其执行get操作。如果thread1想在segment 2上执行put操作,thread2想在segment 4上执行put操作,那么这里是允许的。也就是说,16 个线程一次可以对 ConcurrentHashMap 执行更新(放置/删除)操作。这样这里的等待时间就会减少。因此性能相对优于 synchronisedHashmap 和 Hashtable。
并发哈希映射
ConcurrentHashMap 用于性能关键型应用程序,其中写入操作远多于读取操作。
它是线程安全的,无需同步整个地图。
使用锁完成写入时,读取可能会非常快。
在对象级别没有锁定。
锁定在哈希图存储桶级别的粒度要细得多。
如果一个线程尝试修改它而另一个线程正在对其进行迭代,则 ConcurrentHashMap 不会引发 ConcurrentModificationException。
ConcurrentHashMap 使用大量锁。
读操作是非阻塞的,而写操作对特定的段或存储桶进行锁定。
同步HashMap
对象级别的同步。
每个读/写操作都需要获取锁。
锁定整个集合是一种性能开销。
这实质上只允许访问整个映射的一个线程并阻止所有其他线程。
它可能会引起争用。
SynchronizedHashMap 返回迭代器,它在并发修改时快速失败。
Collection.synchronizedMap()
Collections 实用程序类提供了对集合进行操作并返回包装集合的多态算法。它的 synchronizedMap() 方法提供了线程安全的功能。
当数据一致性至关重要时,我们需要使用 Collections.synchronizedMap()。
ConcurrentHashMap 针对并发访问进行了优化。
访问不会锁定整个地图,而是使用更细粒度的策略,从而提高可伸缩性。还有专门针对并发访问的功能改进,例如并发迭代器。
除了它提供的并发功能(即故障安全迭代器)之外,ConcurrentHashMap
还有一个一个关键功能需要注意。我看到开发人员使用 ConcurrentHashMap
只是因为他们想编辑条目集 - 在迭代时放置/删除。 Collections.synchronizedMap(Map)
不提供 fail-safe 迭代器,而是提供 fail-fast 迭代器。快速失败迭代器使用在迭代期间无法编辑的地图大小的快照。
如果数据一致性非常重要 - 使用 Hashtable 或 Collections.synchronizedMap(Map)。如果速度/性能非常重要并且数据更新可能会受到影响 - 使用 ConcurrentHashMap。
一般来说,如果您想使用 ConcurrentHashMap
,请确保您准备好错过“更新”
(即打印 HashMap 的内容并不能确保它会打印最新的地图)并使用 API,例如CyclicBarrier
以确保整个程序生命周期的一致性。
Collections.synchronizedMap() 方法同步了 HashMap 的所有方法,并有效地将其简化为一个线程一次可以进入的数据结构,因为它将每个方法都锁定在一个公共锁上。
在 ConcurrentHashMap 中,同步的方式稍有不同。 ConcurrentHashMap 不是将每个方法都锁定在一个公共锁上,而是对单独的桶使用单独的锁,因此只锁定 Map 的一部分。默认情况下,有 16 个存储桶,并且为不同的存储桶提供单独的锁。所以默认并发级别是 16。这意味着理论上任何给定时间 16 个线程都可以访问 ConcurrentHashMap,如果它们都将要分离的存储桶。
ConcurrentHashMap 作为并发包的一部分在 Java 1.5 中作为 Hashtable 的替代品出现。使用ConcurrentHashMap,不仅可以在并发多线程环境中安全使用,而且提供比Hashtable和synchronizedMap更好的性能,是您更好的选择。 ConcurrentHashMap 性能更好,因为它锁定了 Map 的一部分。它允许并发读取操作,同时通过同步写入操作来保持完整性。
ConcurrentHashMap 是如何实现的
ConcurrentHashMap 是作为 Hashtable 的替代品而开发的,它以附加的能力支持 Hashtable 的所有功能,即所谓的并发级别。 ConcurrentHashMap 允许多个读取器同时读取而不使用块。通过将 Map 分成不同的部分并在更新中仅阻止 Map 的一部分,这成为可能。默认情况下,并发级别为 16,因此 Map 被拆分为 16 个部分,每个部分由单独的块管理。这意味着,如果它们与 Map 的不同部分一起工作,则 16 个线程可以同时与 Map 一起工作。它使 ConcurrentHashMap 高效,并且不会降低线程安全性。
如果您对 ConcurrentHashMap 的一些重要特性以及何时应该使用 Map 的这种实现感兴趣 - 我只是放了一篇好文章的链接 - How to use ConcurrentHashMap in Java
除了建议的内容外,我还想发布与 SynchronizedMap
相关的源代码。
为了使 Map
线程安全,我们可以使用 Collections.synchronizedMap
语句并输入地图实例作为参数。
Collections
中 synchronizedMap
的实现如下
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
对象的主要方法添加单个锁。所有被锁保护的方法不能被多个线程同时访问。这意味着像 put
和 get
这样的正常操作可以由单个线程同时对 Map
对象中的所有数据执行。
它现在使 Map
对象线程安全,但在某些情况下性能可能会成为问题。
ConcurrentMap
的实现要复杂得多,具体可以参考Building a better HashMap。简而言之,它的实现同时考虑了线程安全和性能。
不定期副业成功案例分享