从线程安全角度解析Hashtable,HashMap,ConcurrentHashMap之间的区别
在 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 的性能痛点。
更多推荐




所有评论(0)