ChatGPT解决这个技术问题 Extra ChatGPT

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

我有一个 Map 将由多个线程同时修改。

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

哈希表

Collections.synchronizedMap(地图)

并发哈希映射

据我了解,Hashtable 是一个旧实现(扩展过时的 Dictionary 类),后来经过调整以适应 Map 接口。虽然它同步的,但它似乎有严重的scalability issues,并且不鼓励用于新项目。

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

@SmilesinaJar 链接目前已损坏,这是本文的存档副本:Why ConcurrentHashMap is better than Hashtable and just as good as a HashMap
IBM:ConcurrentHashMap 如何在不影响线程安全的情况下提供更高的并发性@ibm.com/developerworks/java/library/j-jtp08223/…
仅供参考,Java 6 将 ConcurrentSkipListMap 作为另一个线程安全的 Map 实现。设计为在负载下高度并发,使用 Skip List 算法。

Y
Yuval Adam

根据您的需要,请使用 ConcurrentHashMap。它允许从多个线程同时修改 Map 而无需阻塞它们。 Collections.synchronizedMap(map) 创建一个会降低性能的阻塞 Map,但可以确保一致性(如果使用得当)。

如果您需要确保数据一致性,请使用第二个选项,并且每个线程都需要拥有最新的地图视图。如果性能很关键,则使用第一个,并且每个线程只将数据插入到映射中,读取发生的频率较低。


查看源代码,同步映射只是一个具有一个互斥锁(阻塞)的实现,而 ConcurrentHashMap 处理并发访问更复杂
另请注意,ConcurrentHashMap 不允许空键或空值。所以它们不是同步地图的同等替代品。
@AbdullahShaikh 该文章中提出的问题已在 Java 7 中修复,并且在 Java 8 中进行了进一步改进。
@hengxin:一旦您执行包含多个查询或更新地图的操作,或者当您在地图上进行迭代时,您必须在地图上手动同步以确保一致性。同步映射仅保证映射上的单个操作(方法调用)的一致性,这使得它通常毫无价值,因为大多数现实生活中的操作都不是微不足道的,因此无论如何您都必须手动同步。
Z
Zombies
╔═══════════════╦═══════════════════╦═══════════════════╦═════════════════════╗
║   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
同步地图呢?
Collections.syncronizedMap 的行为类似于支持映射,除了所有方法都是线程安全的
我会打印这张桌子并以每张 5 美元的价格出售;)。好一个@shevchyk
编辑:两者都不是完全线程安全的。这对新开发人员来说有点误导。请参阅:ibm.com/developerworks/java/library/j-jtp07233/index.html 以了解即使 ConcurrentHashMap 对于外部数据竞争也不是完全线程安全的。 (例如:1 个线程删除一个值,然后另一个线程尝试检查它是否存在,如果不存在则放置它。这是一个数据竞争条件,仍然意味着尽管使用了“ConcurrentHashMap”,但您并没有缓解所有线程安全问题。
M
Michael Borgwardt

Hashtable 的“可扩展性问题”在 Collections.synchronizedMap(Map) 中以完全相同的方式存在 - 它们使用非常简单的同步,这意味着只有一个线程可以同时访问映射。

当您进行简单的插入和查找时,这不是什么大问题(除非您非常密集地执行此操作),但是当您需要遍历整个 Map 时,这会成为一个大问题,这对于大型 Map 可能需要很长时间 - 而一个线程执行此操作,所有其他线程如果要插入或查找任何内容,则必须等待。

ConcurrentHashMap 使用非常复杂的技术来减少同步需求,并允许多个线程在不同步的情况下进行并行读取访问,更重要的是,它提供了一个不需要同步的 Iterator,甚至允许在交互期间修改 Map(尽管它不保证在迭代期间插入的元素是否会被返回)。


现在这就是我想要的! :) 不同步的迭代器只是纯粹的甜蜜!谢谢你的信息! :) (:
很好的答案..但这是否意味着在检索线程期间不会获得最新更新,因为阅读器线程不同步。
@MrA:您是在询问 ConcurrentHashMap 吗? “检索”是什么意思?
@Michael Borgwardt 用于 ConcurrentHashmap,例如。假设有多个线程。其中一些正在更新地图,其中一些正在从同一张地图中获取数据。因此,在这种情况下,当线程尝试读取时,可以保证它们将获得已更新的最新数据,因为读取器线程不必持有锁。
A
Ahmed Ashour

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

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

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

one post 证明了这两者的区别,还有 ConcurrentSkipListMap


S
SkyWalker

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

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

我找到了一个 blog entry,它复制了优秀书籍 Java Concurrency In Practice 中的一个表格,我强烈推荐这本书。

Collections.synchronizedMap 仅在您需要包装具有其他一些特征的地图时才有意义,也许是某种有序地图,例如 TreeMap。


是的-似乎我在我所做的所有其他答案中都提到了那本书!
@BillMichell 链接已损坏
@Govinda 在访问链接之前关闭 javascript。博文还在!
R
Ramesh Papaganti

同步地图:

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 允许一个空键。

类似链接

Link1

Link2

Performance Comparison


B
Bill the Lizard

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


Z
Zach Scrivena

Hashtable 和 ConcurrentHashMap 不允许空键或空值。

Collections.synchronizedMap(Map) 同步所有操作(get、put、size 等)。

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

像往常一样,涉及到并发——开销——速度的权衡。您确实需要考虑应用程序的详细并发要求才能做出决定,然后测试您的代码以查看它是否足够好。


e
eljenso

您对 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 文章以更好地了解其复杂性。


R
Raj

这里有几个:

1) ConcurrentHashMap 只锁定 Map 的一部分,而 SynchronizedMap 锁定整个 MAp。 2) ConcurrentHashMap 比 SynchronizedMap 性能更好,可扩展性更强。 3)如果是多个阅读器和单个作者 ConcurrentHashMap 是最好的选择。

此文字来自 Difference between ConcurrentHashMap and hashtable in Java


S
Sumanth Varada

我们可以通过使用 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。


,1。如果多个线程试图编辑同一个块会发生什么? 2. 如果说两个线程试图从同一个块中读取数据,而另一个线程同时写入数据,会发生什么?
P
Premraj

并发哈希映射

ConcurrentHashMap 用于性能关键型应用程序,其中写入操作远多于读取操作。

它是线程安全的,无需同步整个地图。

使用锁完成写入时,读取可能会非常快。

在对象级别没有锁定。

锁定在哈希图存储桶级别的粒度要细得多。

如果一个线程尝试修改它而另一个线程正在对其进行迭代,则 ConcurrentHashMap 不会引发 ConcurrentModificationException。

ConcurrentHashMap 使用大量锁。

读操作是非阻塞的,而写操作对特定的段或存储桶进行锁定。

同步HashMap

对象级别的同步。

每个读/写操作都需要获取锁。

锁定整个集合是一种性能开销。

这实质上只允许访问整个映射的一个线程并阻止所有其他线程。

它可能会引起争用。

SynchronizedHashMap 返回迭代器,它在并发修改时快速失败。

Collection.synchronizedMap()

Collections 实用程序类提供了对集合进行操作并返回包装集合的多态算法。它的 synchronizedMap() 方法提供了线程安全的功能。

当数据一致性至关重要时,我们需要使用 Collections.synchronizedMap()。

source


s
starblue

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

访问不会锁定整个地图,而是使用更细粒度的策略,从而提高可伸缩性。还有专门针对并发访问的功能改进,例如并发迭代器。


r
realPK

除了它提供的并发功能(即故障安全迭代器)之外,ConcurrentHashMap 还有一个一个关键功能需要注意。我看到开发人员使用 ConcurrentHashMap 只是因为他们想编辑条目集 - 在迭代时放置/删除。 Collections.synchronizedMap(Map) 不提供 fail-safe 迭代器,而是提供 fail-fast 迭代器。快速失败迭代器使用在迭代期间无法编辑的地图大小的快照。


C
Christophe Roussy

如果数据一致性非常重要 - 使用 Hashtable 或 Collections.synchronizedMap(Map)。如果速度/性能非常重要并且数据更新可能会受到影响 - 使用 ConcurrentHashMap。


K
Kounavi

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


B
Brad Larson

Collections.synchronizedMap() 方法同步了 HashMap 的所有方法,并有效地将其简化为一个线程一次可以进入的数据结构,因为它将每个方法都锁定在一个公共锁上。

在 ConcurrentHashMap 中,同步的方式稍有不同。 ConcurrentHashMap 不是将每个方法都锁定在一个公共锁上,而是对单独的桶使用单独的锁,因此只锁定 Map 的一部分。默认情况下,有 16 个存储桶,并且为不同的存储桶提供单独的锁。所以默认并发级别是 16。这意味着理论上任何给定时间 16 个线程都可以访问 ConcurrentHashMap,如果它们都将要分离的存储桶。


O
Oleg Poltoratskii

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


E
Eugene

除了建议的内容外,我还想发布与 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的实现要复杂得多,具体可以参考Building a better HashMap。简而言之,它的实现同时考虑了线程安全和性能。