簡體   English   中英

ConcurrentHashMap 和 Collections.synchronizedMap(Map) 有什么區別?

[英]What's the difference between ConcurrentHashMap and Collections.synchronizedMap(Map)?

我有一個要由多個線程同時修改的 Map。

Java API 中似乎有三種不同的同步 Map 實現:

  • Hashtable
  • Collections.synchronizedMap(Map)
  • ConcurrentHashMap

據我了解, Hashtable是一個舊實現(擴展過時的Dictionary類),后來對其進行了調整以適應Map接口。 雖然它同步的,但它似乎存在嚴重的 可擴展性問題,不鼓勵用於新項目。

但是另外兩個呢? Collections.synchronizedMap(Map)ConcurrentHashMap返回的Collections.synchronizedMap(Map)之間有什么區別? 哪個適合哪種情況?

根據您的需要,請使用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 鎖定對象,而ConcurrentHashMap 鎖定存儲桶

Hashtable的“可擴展性問題”在Collections.synchronizedMap(Map)以完全相同的方式存在 - 它們使用非常簡單的同步,這意味着只有一個線程可以同時訪問映射。

當您進行簡單的插入和查找時,這不是什么大問題(除非您非常密集地進行),但是當您需要遍歷整個 Map 時,這會成為一個大問題,這對於大型 Map 可能需要很長時間 - 而一個線程這樣做,所有其他線程如果要插入或查找任何內容都必須等待。

ConcurrentHashMap使用非常復雜的技術來減少同步的需要,並允許多個線程在沒有同步的情況下並行讀取訪問,更重要的是,它提供了一個不需要同步的Iterator ,甚至允許在交互期間修改 Map(盡管它不保證是否會返回在迭代期間插入的元素)。

這兩者的主要區別在於ConcurrentHashMap只會鎖定正在更新的部分數據,而其他部分的數據可以被其他線程訪問。 但是, Collections.synchronizedMap()會在更新時鎖定所有數據,其他線程只有在鎖定釋放后才能訪問數據。 如果更新操作比較多,讀操作比較少,應該選擇ConcurrentHashMap

另外一個不同點是ConcurrentHashMap不會保留傳入Map中元素的順序。它在存儲數據時類似於HashMap 不能保證保留元素順序。 雖然Collections.synchronizedMap()將保持在地圖傳遞的元素順序。例如,如果你傳遞一個TreeMapConcurrentHashMap ,這些元素才能在ConcurrentHashMap可能不一樣,在順序TreeMap ,但Collections.synchronizedMap()將保留順序。

此外, ConcurrentHashMap可以保證在一個線程正在更新映射而另一個線程正在遍歷從映射中獲取的迭代器時不會拋出ConcurrentModificationException 但是, Collections.synchronizedMap()不能保證這一點。

一篇文章展示了這兩者的差異以及ConcurrentSkipListMap

當您可以使用 ConcurrentHashMap 時,首選它 - 盡管它至少需要 Java 5。

它被設計為在被多線程使用時可以很好地擴展。 當一次只有一個線程訪問 Map 時,性能可能會稍差一些,但當多個線程同時訪問 Map 時,性能會好得多。

我找到了一個博客條目,它從優秀的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,而是將映射划分為段並鎖定這些段。 如果讀取器線程數大於寫入器線程數,則性能更好。

ConcurrentHashMap 默認分為 16 個區域並應用鎖。 這個默認數字可以在初始化 ConcurrentHashMap 實例時設置。 在特定段中設置數據時,將獲得該段的鎖定。 這意味着如果兩個更新分別影響不同的存儲桶,它們仍然可以同時安全地執行,從而最大限度地減少鎖爭用,從而最大限度地提高性能。

ConcurrentHashMap 不會拋出 ConcurrentModificationException

如果一個線程試圖修改它而另一個正在迭代它,則 ConcurrentHashMap 不會拋出 ConcurrentModificationException

synchornizedMap 和 ConcurrentHashMap 的區別

Collections.synchornizedMap(HashMap) 將返回一個幾乎等同於 Hashtable 的集合,其中 Map 上的每個修改操作都鎖定在 Map 對象上,而在 ConcurrentHashMap 的情況下,通過根據並發級別將整個 Map 划分為不同的分區來實現線程安全並且只鎖定特定部分而不是鎖定整個地圖。

ConcurrentHashMap 不允許空鍵或空值,而同步 HashMap 允許一個空鍵。

類似鏈接

鏈接1

鏈接2

性能比較

ConcurrentHashMap ,鎖定應用於一個段而不是整個 Map。 每個段管理自己的內部哈希表。 該鎖僅適用於更新操作。 Collections.synchronizedMap(Map)同步整個地圖。

像往常一樣,涉及並發-開銷-速度權衡。 您確實需要考慮應用程序的詳細並發要求來做出決定,然后測試您的代碼,看看它是否足夠好。

你是對的HashTable ,你可以忘記它。

您的文章提到了一個事實,雖然 HashTable 和同步包裝類通過一次只允許一個線程訪問映射來提供基本的線程安全,但這不是“真正的”線程安全,因為許多復合操作仍然需要額外的同步,例如例子:

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

但是,不要認為ConcurrentHashMap是具有如上所示的典型synchronized塊的HashMap的簡單替代方案。 閱讀 文章,了解其復雜性更好。

我們可以通過使用 ConcurrentHashMap 和 SynchronisedHashmap 和 Hashtable 來實現線程安全。 但是如果你看看他們的架構,就會有很多不同。

  1. 同步Hashmap和Hashtable

兩者都將在對象級別保持鎖定。 所以如果你想執行像 put/get 這樣的任何操作,那么你必須先獲取鎖。 同時,不允許其他線程執行任何操作。 因此,一次只有一個線程可以對此進行操作。 所以這里的等待時間會增加。 可以說和 ConcurrentHashMap 相比,性能比較低。

  1. 並發哈希映射

它將保持段級別的鎖定。 它有 16 個段,並且默認保持並發級別為 16。 所以一次可以有 16 個線程可以操作 ConcurrentHashMap。 而且,讀操作不需要鎖。 因此,任意數量的線程都可以對其執行 get 操作。

如果thread1想在segment 2執行put操作,thread2想在segment 4執行put操作,這里是允許的。 意味着,16 個線程可以同時對 ConcurrentHashMap 執行更新(放置/刪除)操作。

這樣在這里等待的時間就會少一些。 因此性能相對優於同步哈希圖和哈希表。

這里有幾個:

1) ConcurrentHashMap 只鎖定 Map 的一部分,而 SynchronizedMap 鎖定整個 MAp。
2) ConcurrentHashMap 比 SynchronizedMap 有更好的性能和更多的可擴展性。
3) 在多讀者和單作者的情況下 ConcurrentHashMap 是最好的選擇。

本文來自Java ConcurrentHashMap 和 hashtable 的區別

並發哈希映射

  • ConcurrentHashMap 適用於寫入操作遠多於讀取操作的性能關鍵型應用程序。
  • 它是線程安全的,無需同步整個地圖。
  • 當使用鎖完成寫入時,讀取可以非常快地發生。
  • 在對象級別沒有鎖定。
  • 鎖定在哈希圖存儲桶級別的粒度要細得多。
  • 如果一個線程試圖修改它而另一個正在迭代它,則 ConcurrentHashMap 不會拋出 ConcurrentModificationException。
  • ConcurrentHashMap 使用多個鎖。
  • 讀操作是非阻塞的,而寫操作在特定的段或桶上鎖定。

同步哈希映射

  • 對象級別的同步。
  • 每個讀/寫操作都需要獲取鎖。
  • 鎖定整個集合是一種性能開銷。
  • 這基本上只允許一個線程訪問整個地圖並阻止所有其他線程。
  • 它可能會引起爭用。
  • SynchronizedHashMap 返回迭代器,它在並發修改時快速失敗。

Collection.synchronizedMap()

  • Collections 實用程序類提供了對集合進行操作並返回包裝的集合的多態算法。 它的 synchronizedMap() 方法提供了線程安全的功能。
  • 當數據一致性至關重要時,我們需要使用 Collections.synchronizedMap()。

來源

除了ConcurrentHashMap提供的並發特性之外,還有一個需要注意的關鍵特性,即故障安全迭代器。 我見過開發人員使用ConcurrentHashMap只是因為他們想編輯條目集 - 在迭代它時放置/刪除。 Collections.synchronizedMap(Map)不提供故障安全迭代器,而是提供故障快速迭代器。 快速失敗迭代器使用無法在迭代過程中編輯的地圖大小的快照。

ConcurrentHashMap 針對並發訪問進行了優化。

訪問不會鎖定整個地圖,而是使用更細粒度的策略,這提高了可擴展性。 還有專門用於並發訪問的功能增強,例如並發迭代器。

  1. 如果數據一致性非常重要 - 使用 Hashtable 或 Collections.synchronizedMap(Map)。
  2. 如果速度/性能非常重要並且數據更新可能會受到影響 - 使用 ConcurrentHashMap。

一般來說,如果你想使用ConcurrentHashMap確保你准備好錯過“更新”
(即打印 HashMap 的內容並不能確保它會打印最新的 Map)並使用CyclicBarrier API 來確保整個程序生命周期的一致性。

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 個部分,每個部分由單獨的塊管理。 這意味着,16 個線程可以同時使用 Map,如果它們使用 Map 的不同部分。 它使 ConcurrentHashMap 高效,並且不會降低線程安全性。

如果您對 ConcurrentHashMap 的一些重要功能感興趣,以及何時應該使用 Map 的這種實現 - 我只是放了一篇好文章的鏈接 - 如何在 Java 中使用 ConcurrentHashMap

除了建議的內容之外,我還想發布與SynchronizedMap相關的源代碼。

為了使Map線程安全,我們可以使用Collections.synchronizedMap語句並輸入 map 實例作為參數。

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 簡而言之,它的實現同時考慮了線程安全和性能。

性能比較

我使用開源框架 Java Microbenchmark Harness (JMH) 以納秒為單位比較了這些方法的性能。

@Benchmark
public void randomReadAndWriteSynchronizedMap() {
    Map<String, Integer> map = Collections.synchronizedMap(new HashMap<String, Integer>());
    performReadAndWriteTest(map);
}

@Benchmark
public void randomReadAndWriteConcurrentHashMap() {
    Map<String, Integer> map = new ConcurrentHashMap<>();
    performReadAndWriteTest(map);
}

private void performReadAndWriteTest(final Map<String, Integer> map) {
    for (int i = 0; i < TEST_NO_ITEMS; i++) {
        Integer randNumber = (int) Math.ceil(Math.random() * TEST_NO_ITEMS);
        map.get(String.valueOf(randNumber));
        map.put(String.valueOf(randNumber), randNumber);
    }
}

1000 個項目使用5 次迭代10 個線程的性能基准測試。 讓我們看看基准測試結果:

Benchmark                                                     Mode  Cnt        Score        Error  Units
MapPerformanceComparison.randomReadAndWriteConcurrentHashMap  avgt  100  3061555.822 ±  84058.268  ns/op
MapPerformanceComparison.randomReadAndWriteSynchronizedMap    avgt  100  3234465.857 ±  60884.889  ns/op
MapPerformanceComparison.randomReadConcurrentHashMap          avgt  100  2728614.243 ± 148477.676  ns/op
MapPerformanceComparison.randomReadSynchronizedMap            avgt  100  3471147.160 ± 174361.431  ns/op
MapPerformanceComparison.randomWriteConcurrentHashMap         avgt  100  3081447.009 ±  69533.465  ns/op
MapPerformanceComparison.randomWriteSynchronizedMap           avgt  100  3385768.422 ± 141412.744  ns/op

以上結果表明ConcurrentHashMap 的性能優於 Collections.synchronizedMap()

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM