关于Java:ConcurrentHashMap和Collections.synchronizedMap(Map)有什么区别?

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

我有一个映射,它将被几个线程同时修改。

在Java API中似乎有三种不同的同步映射实现:

  • Hashtable
  • Collections.synchronizedMap(Map)
  • ConcurrentHashMap

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

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


根据您的需要,使用ConcurrentHashMap。它允许从多个线程并发修改映射,而不需要阻塞它们。Collections.synchronizedMap(map)创建一个阻塞映射,它将降低性能,尽管确保一致性(如果使用得当)。

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


1
2
3
4
5
6
7
8
9
10
11
12
13
╔═══════════════╦═══════════════════╦═══════════════════╦═════════════════════╗
║   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               ║ weakly consistent   ║
╚═══════════════╩═══════════════════════════════════════╩═════════════════════╝

关于锁紧机构:Hashtable锁定对象,而ConcurrentHashMap只锁定bucket。


Hashtable的"可伸缩性问题"在Collections.synchronizedMap(map)中以完全相同的方式出现,它们使用非常简单的同步,这意味着只有一个线程可以同时访问映射。

当您进行简单的插入和查找时(除非您非常密集地进行插入和查找),这不是一个很大的问题,但当您需要对整个映射进行迭代时,这将成为一个大问题,对于一个大型映射来说,这可能需要很长的时间-虽然一个线程会这样做,但如果其他线程想要插入或查找任何内容,则必须等待。

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


当您可以使用CONTURNEASHMAP时,尽管它至少需要Java 5。

它被设计成在多线程使用时能够很好地缩放。当一次只有一个线程访问映射时,性能可能会稍差一些,但当多个线程同时访问映射时,性能会显著提高。

我发现了一个博客条目,它从实践中的优秀图书Java并发复制了一个表,我对此进行了彻底的推荐。

collections.synchronizedMap只有在需要使用其他一些特性(可能是某种有序的映射,如treemap)来包装映射时才有意义。


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

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

此外,当一个线程正在更新映射,另一个线程正在遍历从映射中获得的迭代器时,ConcurrentHashMap可以保证不抛出ConcurrentModificationException。但是,Collections.synchronizedMap()对此不作保证。

有一篇文章展示了这两篇文章的不同之处,还有ConcurrentSkipListMap


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


  • HashtableConcurrentHashMap不允许null键或null值。

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

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

和往常一样,这里涉及到并发性——开销——加速权衡。您真的需要考虑应用程序的详细并发性需求来做出决定,然后测试代码,看看它是否足够好。


你说得对,你可以忘记。

您的文章提到了这样一个事实:虽然hashtable和synchronized wrapper类通过一次只允许一个线程访问映射来提供基本的线程安全性,但这不是"真的"线程安全性,因为许多复合操作仍然需要额外的同步,例如:

1
2
3
4
5
6
7
8
synchronized (records) {
  Record rec = records.get(id);
  if (rec == null) {
      rec = new Record(id);
      records.put(id, rec);
  }
  return rec;
}

然而,不要认为ConcurrentHashMap是一个简单的替代方法,以一个典型的synchronized区块的HashMap如上图所示。阅读这篇文章,更好地理解它的复杂性。


同步映射:

同步映射也与Hashtable没有很大的不同,并在并发Java程序中提供类似的性能。Hashtable和SynchronizedMap之间的唯一区别是,SynchronizedMap不是旧版,您可以使用collections.synchronizedMap()方法包装任何映射以创建其同步版本。

当前地图:

ConcurrentHashMap类提供标准HashMap的并发版本。这是对Collections类中提供的SynchronizedMap功能的改进。

与hashtable和synchronized映射不同,它从不锁定整个映射,而是将映射分段,并对其进行锁定。如果读线程数大于写线程数,则性能更好。

ConcurrentHashMap默认分为16个区域,并应用锁。可以在初始化ConcurrentHashMap实例时设置此默认数字。在特定段中设置数据时,将获取该段的锁。这意味着,如果两个更新各自影响不同的存储桶,那么它们仍然可以同时安全地执行,从而最小化锁争用,从而最大限度地提高性能。

ConcurrentHashMap不会引发ConcurrentModificationException

如果一个线程在另一个线程对ConcurrentModificationException进行迭代时试图对其进行修改,那么ConcurrentHashMap不会抛出ConcurrentModificationException。

同步化映射与同步化映射的区别

collections.synchronizedmap(hashmap)将返回一个几乎等同于hashtable的集合,其中map上的每个修改操作都锁定在map对象上,而在concurrenthashmap的情况下,通过根据并发级别将整个map划分为不同的分区,并且只锁定特定的部分inste来实现线程安全性。锁定整个地图的广告。

ConcurrentHashMap不允许空键或空值,而同步HashMap允许一个空键。

相似环节

LIK1

Link 2

性能比较


这里有几个:

1)Concurrenthashmap只锁定映射的一部分,而Synchronizedmap锁定整个映射。2)Concurrenthashmap比SynchronizedMap性能更好,可扩展性更强。3)在多个读者和单个作者同时阅读的情况下,地图是最佳选择。

本文从Java中的并发冲突映射和哈希表的区别出发。


我们可以通过使用concurrenthashmap、synchronizedhashmap和hashtable来实现线程安全。但是如果你看看他们的架构,会发现有很多不同。

  • 同步hashmap和hashtable
  • Both will maintain the lock at the object level. So if you want to perform any operation like put/get then you have to acquire the lock first. At the same time, other threads are not allowed to perform any operation. So at a time, only one thread can operate on this. So the waiting time will increase here. We can say that performance is relatively low when you comparing with ConcurrentHashMap.

  • 当前地图
  • It will maintain the lock at segment level. It has 16 segments and maintains the concurrency level as 16 by default. So at a time, 16 threads can be able to operate on ConcurrentHashMap. Moreover, read operation doesn't require a lock. So any number of threads can perform a get operation on it.

    If thread1 wants to perform put operation in segment 2 and thread2 wants to perform put operation on segment 4 then it is allowed here. Means, 16 threads can perform update(put/delete) operation on ConcurrentHashMap at a time.

    So that the waiting time will be less here. Hence the performance is relatively better than synchronisedHashmap and Hashtable.


    当前地图

    • 当您在项目中需要非常高的并发性时,应该使用ConcurrentHashMap。
    • 它是线程安全的,不同步整个映射。
    • 当使用锁完成写操作时,读取速度可能非常快。
    • 对象级别没有锁定。
    • 在hashmap bucket级别,锁定的粒度要小得多。
    • 如果一个线程试图修改它,而另一个线程正在对其进行迭代,那么ConcurrentHashMap不会抛出ConcurrentModificationException。
    • ConcurrentHashMap使用多个锁。

    同步哈希映射

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

    来源


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

    访问不会锁定整个映射,而是使用更细粒度的策略,这可以提高可伸缩性。还有专门针对并发访问的功能增强,例如并发迭代器。


    除了它提供的并发功能(即故障安全迭代器)之外,还有一个关于ConcurrentHashMap的关键功能需要注意。我见过开发人员使用ConcurrentHashMap仅仅是因为他们想在迭代过程中编辑entryset-put/remove。Collections.synchronizedMap(Map)不提供故障安全迭代器,而是提供故障快速迭代器。fail fast迭代器使用在迭代期间无法编辑的映射大小快照。


  • 如果数据一致性非常重要-请使用hashtable或collections.SynchronizedMap(映射)。
  • 如果速度/性能非常重要,并且数据更新可能受到影响,请使用Concurrenthashmap。

  • 一般来说,如果您想使用ConcurrentHashMap,请确保您准备好错过'updates'
    (即,打印hashmap的内容并不确保它将打印最新的映射),并使用CyclicBarrier之类的API来确保整个程序生命周期的一致性。


    collections.synchronizedMap()方法同步hashmap的所有方法,并有效地将其减少到一个数据结构,在该结构中,一个线程可以一次进入,因为它将每个方法锁定在一个公共锁上。

    在ConcurrentHashMap中,同步的执行方式略有不同。ConcurrentHashMap不将每个方法都锁定在一个公共锁上,而是对单独的bucket使用单独的锁,从而只锁定映射的一部分。默认情况下,有16个存储桶,并且对于单独的存储桶也有单独的锁。所以默认并发级别是16。这意味着理论上,如果16个线程都要分离bucket,那么它们可以访问ConcurrentHashMap。


    除了建议之外,我还想发布与SynchronizedMap相关的源代码。

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

    CollectionsSynchronizedMap的实现如下

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

    如您所见,输入Map对象由SynchronizedMap对象包装。让我们深入研究SynchronizedMap的实现,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
     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对象的主方法添加一个锁。锁保护的所有方法不能同时被多个线程访问。这意味着,对于Map对象中的所有数据,可以通过单个线程同时执行像putget这样的正常操作。

    它现在使Map对象线程安全,但在某些情况下性能可能会成为一个问题。

    ConcurrentMap在实现上要复杂得多,我们可以参考构建更好的hashmap了解详细信息。简而言之,它的实现考虑了线程安全和性能。