ConcurrentHashMap性能优于Collections.synchronizedMap,因其采用CAS与synchronized结合的细粒度锁机制,支持高并发读写;而synchronizedMap使用全局锁,导致高并发下线程阻塞严重。前者在JDK 8中以桶为单位加锁,读操作无锁,写操作仅锁定冲突桶,并支持链表转红黑树优化性能;后者所有方法均同步,吞吐量低。此外,ConcurrentHashMap不支持null键值,提供原子复合操作如putIfAbsent,迭代器弱一致性;synchronizedMap允许null键值,迭代器快速失败,复合操作需外部同步。低并发或需快速失败迭代器时可选synchronizedMap,但多数场景ConcurrentHashMap更优。

在Java并发编程的语境下,
ConcurrentHashMap和
Collections.synchronizedMap都是为了解决多线程环境下
HashMap的非线程安全问题而生。但要论“终极性能”,我个人会毫不犹豫地把票投给
ConcurrentHashMap。它在设计之初就考虑了高并发场景下的性能和可伸缩性,而
synchronizedMap更多的是一种“打补丁”式的解决方案,用全局锁来简单粗暴地实现线程安全,这在高并发下往往会成为性能瓶颈。
解决方案
要深入理解两者的性能差异,我们得从它们实现线程安全的底层机制说起。
Collections.synchronizedMap(new HashMap<>())的工作原理非常直接:它返回一个
Map的包装器,这个包装器中的所有公共方法(包括
get、
put、
remove、
size等)都被
synchronized关键字修饰,锁住的是这个包装器对象本身。这意味着在任何给定时刻,只有一个线程能够访问这个
Map的任何一个操作。想象一下,你有一个巨大的图书馆,但只有一个入口,所有读者(线程)无论是借书、还书还是查阅,都必须排队通过这唯一的入口。在高并发场景下,这种全局锁机制会导致严重的竞争,大量线程会因为等待锁而被阻塞,从而极大地降低吞吐量和性能。
而
ConcurrentHashMap的设计哲学则完全不同。它采取了一种更精细、更智能的并发控制策略。在JDK 7及之前,
ConcurrentHashMap采用了分段锁(Segment Locking)的机制,将整个
HashMap分割成若干个段(Segment),每个段都是一个独立的
HashEntry数组,并拥有自己的锁。这样,不同的线程就可以同时访问不同的段,进行读写操作,大大减少了锁的粒度。就好比图书馆里有多个独立的阅览室,每个阅览室都有自己的门禁,读者可以同时进入不同的阅览室。
到了JDK 8,
ConcurrentHashMap的实现进一步优化,彻底放弃了分段锁,转而采用了一种更加细粒度的并发控制:结合了CAS(Compare-And-Swap)操作和
synchronized关键字。它将
HashMap的桶(bin)作为基本的锁单元。对于非冲突的操作(比如对不同桶的写入),
ConcurrentHashMap可以通过CAS操作实现无锁化;而当出现哈希冲突或需要修改特定桶时,它会只对那个桶的头节点进行
synchronized锁定。更妙的是,在大多数读操作中,
ConcurrentHashMap甚至不需要加锁,因为它利用了
volatile关键字和内存屏障来保证数据可见性。这种设计允许大量的并发读操作几乎不受阻塞,并且不同桶之间的写操作也能并行进行,从而在高并发环境下展现出卓越的性能和可伸缩性。
立即学习“Java免费学习笔记(深入)”;
所以,从根本上讲,
synchronizedMap是悲观锁的典型代表,锁的粒度大;而
ConcurrentHashMap则是乐观锁(CAS)与悲观锁(synchronized)的结合,并且锁的粒度极小,甚至在很多情况下避免了锁。这直接决定了
ConcurrentHashMap在处理大量并发请求时,能够提供远超
synchronizedMap的吞吐量和响应速度。
ConcurrentHashMap 到底是如何实现高性能并发的?
要理解
ConcurrentHashMap的高性能,我们得稍微深入它的内部构造。在JDK 8中,
ConcurrentHashMap的核心思想是减少锁的竞争,并尽可能地允许并发操作。
首先,它内部的哈希表结构由
Node组成,每个[] table
Node代表一个键值对。当多个线程尝试修改不同的桶时,它们通常不会相互阻塞。这是因为
ConcurrentHashMap对不同的桶(即
table数组的不同索引位置)使用独立的锁。具体来说,当一个线程需要修改某个桶中的数据时,它会尝试对该桶的头节点进行
synchronized锁定。这意味着,如果两个线程修改的是不同的桶,它们可以并行执行,互不影响。
其次,对于读操作,
ConcurrentHashMap几乎是无锁的。它利用
volatile关键字保证了
Node数组的可见性,使得一个线程写入的数据,其他线程能够立即看到。在
get()方法中,它只是简单地遍历哈希桶,查找对应的键,这个过程不需要任何锁。这种设计极大地提升了读操作的性能,因为读操作是并发集合中最频繁的操作之一。
此外,
ConcurrentHashMap还巧妙地结合了CAS操作来处理一些非冲突的更新。例如,当一个桶为空,第一次插入元素时,它会尝试使用CAS操作来设置
Node。只有当CAS失败(意味着其他线程抢先一步插入了),它才会退回到使用
synchronized锁来保证原子性。
最后,当哈希冲突严重导致某个桶的链表过长时(默认阈值是8),
ConcurrentHashMap会将这个链表转换成红黑树,以保证查找、插入和删除操作的最坏时间复杂度为
O(log n),而不是
O(n)。在对红黑树进行操作时,同样会使用
synchronized锁来保证线程安全。
这种多层次、混合式的并发控制策略,使得
ConcurrentHashMap在绝大多数并发场景下都能表现出卓越的性能。它不是简单地给所有操作加锁,而是根据操作类型和冲突程度,智能地选择最合适的并发控制手段,从而实现了高并发下的高吞吐量和低延迟。
什么时候我们仍然应该考虑使用 SynchronizedMap?
尽管
ConcurrentHashMap在性能上碾压
synchronizedMap,但在某些特定场景下,
synchronizedMap依然有其存在的价值,或者说,使用它并不会带来明显的劣势,甚至可能更简单直观。
在我看来,最主要的情况是低并发环境。如果你的应用程序中,对
Map的并发访问非常少,或者说并发线程数极低,那么
synchronizedMap的全局锁开销可能根本不会成为性能瓶颈。在这种情况下,
ConcurrentHashMap内部更复杂的机制(比如更多的内存开销、CAS操作的循环重试等)反而可能带来微小的额外开销。虽然这个开销通常可以忽略不计,但如果你的首要目标是代码的简洁性和易理解性,
synchronizedMap可能会显得更直接。
其次,当外部已经存在严格的同步机制时。比如,你可能在一个大的
synchronized块内部操作一个
Map,或者你的整个业务逻辑本身就已经是单线程处理的,只是偶尔会被其他线程访问。在这种情况下,
synchronizedMap提供的那层额外同步可能显得多余,但也不会造成伤害,因为它只是提供了一层“保障”。
再者,如果你的业务逻辑对迭代器的一致性有非常严格的要求,并且你能够确保在迭代期间外部不会修改
Map。
synchronizedMap的迭代器是快速失败(fail-fast)的,这意味着如果在迭代过程中
Map被其他线程修改了,它会立即抛出
ConcurrentModificationException。这在某些场景下可以帮助你快速发现并发问题。而
ConcurrentHashMap的迭代器是弱一致性(weakly consistent)的,它可能不会反映迭代器创建之后的所有修改,但也不会抛出异常。这两种行为模式各有优劣,取决于你的具体需求。
最后,遗留系统和兼容性也是一个考虑因素。在一些老旧的项目中,可能已经大量使用了
synchronizedMap,如果性能不是瓶颈,并且重构到
ConcurrentHashMap会带来较大的风险和工作量,那么保持现状也未尝不可。毕竟,工程师的时间和项目的稳定性同样重要。
总的来说,选择
synchronizedMap更多是出于简单性、低并发场景下的足够性以及特定迭代器行为的考量。一旦你预见到有中高并发的可能,或者对性能有哪怕一点点要求,
ConcurrentHashMap几乎总是更优的选择。
除了性能,两者在功能和行为上还有哪些关键差异?
除了性能上的巨大鸿沟,
ConcurrentHashMap和
synchronizedMap在功能和行为上还存在几个关键且容易被忽视的差异,这些差异有时会影响你的编程决策。
第一个显著区别是对 null
键和 null
值的支持。
ConcurrentHashMap不允许
null键或
null值。如果你尝试插入
null键或
null值,它会抛出
NullPointerException。这是出于设计上的考量,
null在并发环境中可能导致歧义和复杂性,比如无法区分一个键是不存在还是其值就是
null。而
synchronizedMap因为内部包装的是
HashMap,所以它允许一个
null键和多个
null值,这与
HashMap的行为保持一致。这个差异在使用时需要特别注意,尤其是在从非并发代码迁移到并发代码时。
第二个是复合操作的原子性。
synchronizedMap的所有单个操作(如
put、
get、
remove)都是原子性的,因为它们都通过全局锁保护。但如果你的操作涉及到多个步骤,例如
if (!map.containsKey(key)) map.put(key, value);这样的“先检查后执行”的复合操作,
synchronizedMap并不能保证整个复合操作的原子性。在
containsKey和
put之间,其他线程仍然可能修改
Map。要保证复合操作的原子性,你仍然需要外部的
synchronized块来包裹整个逻辑。
ConcurrentHashMap同样如此,它的单个操作是线程安全的,但复合操作也需要额外的同步措施。不过,
ConcurrentHashMap提供了一些原子性的复合操作方法,比如
putIfAbsent()、
compute()、
merge()等,这些方法可以在内部以原子方式执行,从而简化了部分复合操作的实现。
第三个是迭代器的一致性模型。前面提到过,
synchronizedMap的迭代器是快速失败(fail-fast)的。这意味着如果在迭代过程中,除了迭代器自身的
remove()方法之外,
Map被任何其他方式(包括其他线程)结构性地修改了,迭代器会立即抛出
ConcurrentModificationException。这有助于在开发阶段发现并发修改的问题。而
ConcurrentHashMap的迭代器是弱一致性(weakly consistent)的。它会反映迭代器创建时
Map的状态,但可能不会反映迭代器创建之后的所有修改,也不会抛出
ConcurrentModificationException。这意味着你可能会看到部分更新,或者错过一些更新,但程序不会崩溃。这种设计是为了在并发环境中提供更高的可用性和吞吐量,但代价是迭代结果的严格一致性。
理解这些非性能层面的差异,对于在特定场景下做出正确的选择至关重要。你不仅仅要考虑“快不快”,还要考虑“好不好用”、“会不会出问题”以及“我的数据模型是否允许
null”。











