在 Java 集合框架中,哈希表是常用的数据结构,但在多线程场景下,不同实现的线程安全特性和性能表现差异显著。本文将聚焦 HashMap 、 Hashtable  和 ConcurrentHashMap  的线程安全特性,分析其设计差异与适用场景。

1. HashMap:非线程安全的“性能优先”实现

HashMap  是日常开发中最常用的哈希表实现,但其设计初衷是单线程场景——本身不具备线程安全性。在多线程环境下,若同时对  HashMap  进行修改(如  put 、 remove  等操作),可能导致链表成环、数据丢失等问题(尤其是在 JDK 1.7 及之前的版本中,扩容时的链表迁移逻辑易引发并发问题)。
 
因此, HashMap  仅适合单线程或通过外部同步(如手动加锁)控制的场景。

2. Hashtable:简单粗暴的线程安全实现

Hashtable只是简单把关键方法加上了synchronized关键字:

public synchronized V put(K key,V value){

public synchronized V get(Object key){

这就相当于直接针对Hashtable对象本身加锁,一个Hashtable只有一把锁,当多个线程访问同一个Hashtable时,访问Hashtable中的任意数据都会直接造成锁竞争,产生冲突。

size属性也是通过synchronized来控制同步,也比较慢

一旦触发扩容,就由该线程完成整个扩容过程,这个过程会涉及大量的元素拷贝,效率会非常低。

然而ConcurrentHashMap对于这些问题做出了一系列的改进和优化。

3. ConcurrentHashMap:高效的并发哈希表(以 JDK 1.8 为例)

读操作没有加锁(但是使用了volatile保证从内存读取结果),只对写操作进行加锁,加锁方式依旧是使用synchronized关键字,但不是锁整个对象,而是“锁桶”(用每个链表的头节点作为锁对象),大大降低了锁冲突的概率,只有当两个线程访问的是同一个哈希桶上的数据才会出现锁冲突。

size属性通过CAS(简单来说就是假设内存中的原数据V,旧的预期值A,需要修改的新值B,比较A与V是否相等,如果相等,将B写入V)来更新,避免出现重量级锁的情况

优化了扩容方式

需要扩容的线程,只需要创建一个新的数组,同时只搬几个元素过去,扩容期间,新老数组同时存在,后续每个来操作ConcurrentHashMap的线程,都会参与搬家的过程,每个操作负责搬运一小部分元素,搬完最后一个元素再把老数组删掉,这个期间插入只往新数组,如果此时有查找操作需要同时查新数组和老数组。

4.总结

特性 HashMap Hashtable ConcurrentHashMap(JDK1.8)
线程安全 是(全局锁) 是(细颗粒度)
锁策略 无锁 全表锁 锁桶+无锁读
并发性能 高(单线程) 低(锁冲突严重) 高(细颗粒锁+无锁读)
使用场景 单线程/外部同步 已基本淘汰 多线程高并发场景

综上,在多线程环境中, ConcurrentHashMap  是哈希表的首选实现,它通过精细化设计平衡了线程安全与并发效率,完美解决了 Hashtable  的性能痛点。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐