引言
在 Java 集合框架中,HashMap、HashTable和ConcurrentHashMap都是基于哈希表实现的 Map 接口实现类,它们在多线程环境下的表现、性能特征和适用场景有着显著差异。理解这三者的区别,对于编写高效、安全的并发程序至关重要。本文将深入剖析它们的底层实现、线程安全性、性能表现以及使用场景。
1. 核心特性对比概览
| 特性 | HashMap | HashTable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | 否 | 是(方法级 synchronized) | 是(分段锁/CAS) |
| 允许 null 键/值 | 是 | 否 | 否(键值均不允许) |
| 初始容量 | 16 | 11 | 16 |
| 扩容因子 | 0.75 | 0.75 | 0.75 |
| 迭代器 | Fail-Fast | Fail-Fast | Weakly Consistent |
| 继承体系 | AbstractMap | Dictionary | AbstractMap |
| Java 版本 | 1.2+ | 1.0+ | 1.5+ |
| 性能 | 单线程最快 | 多线程性能差 | 高并发性能优秀 |
2. HashMap:非线程安全的哈希表
2.1 基本特性
HashMap是 Java 中最常用的 Map 实现,它基于哈希表(数组+链表/红黑树)实现,提供了常数时间复杂度的基本操作(get 和 put)。
2.2 关键实现细节
// HashMap 的 put 方法核心逻辑(简化版)publicVput(Kkey,Vvalue){returnputVal(hash(key),key,value,false,true);}finalVputVal(inthash,Kkey,Vvalue,booleanonlyIfAbsent,booleanevict){Node<K,V>[]tab;Node<K,V>p;intn,i;// 懒加载:第一次 put 时才初始化数组if((tab=table)==null||(n=tab.length)==0)n=(tab=resize()).length;// 计算索引位置:(n-1) & hashif((p=tab[i=(n-1)&hash])==null)tab[i]=newNode(hash,key,value,null);// 直接插入else{// 处理哈希冲突// ... 链表/红黑树插入逻辑}// 检查是否需要扩容if(++size>threshold)resize();returnnull;}2.3 线程安全问题
HashMap不是线程安全的,在多线程环境下同时修改可能导致:
- 数据丢失:多个线程同时 put 可能覆盖彼此的数据
- 死循环:JDK 1.7 及之前版本在扩容时可能形成环形链表
- 大小不一致:size 字段更新不同步
// 线程不安全的示例publicclassHashMapThreadUnsafeDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{Map<String,Integer>map=newHashMap<>();Threadt1=newThread(()->{for(inti=0;i<1000;i++){map.put("key"+i,i);}});Threadt2=newThread(()->{for(inti=1000;i<2000;i++){map.put("key"+i,i);}});t1.start();t2.start();t1.join();t2.join();// 结果可能小于 2000,存在数据丢失System.out.println("Map size: "+map.size());}}3. HashTable:线程安全的遗留类
3.1 设计特点
HashTable是 Java 早期的线程安全 Map 实现,通过在方法上添加synchronized关键字实现线程安全。
// HashTable 的 put 方法(简化)publicsynchronizedVput(Kkey,Vvalue){// 检查 value 不能为 nullif(value==null){thrownewNullPointerException();}// 确保 key 不为 nullEntry<?,?>tab[]=table;inthash=key.hashCode();intindex=(hash&0x7FFFFFFF)%tab.length;// ... 插入逻辑}3.2 性能瓶颈
由于所有方法都是synchronized的,HashTable存在严重的性能问题:
- 锁粒度粗:整个对象一把锁,并发度低
- 竞争激烈:多个线程无法同时读写
- 吞吐量低:高并发场景下性能急剧下降
3.3 使用限制
- 不允许
null键和null值 - 初始容量为 11(质数),扩容为
2n+1 - 继承自
Dictionary类(已过时)
4. ConcurrentHashMap:高并发优化方案
4.1 演进历程
- JDK 1.5-1.7:分段锁(Segment)实现
- JDK 1.8+:CAS + synchronized 优化
4.2 JDK 1.8 实现原理
// ConcurrentHashMap 的 put 方法核心(简化)finalVputVal(Kkey,Vvalue,booleanonlyIfAbsent){if(key==null||value==null)thrownewNullPointerException();inthash=spread(key.hashCode());intbinCount=0;for(Node<K,V>[]tab=table;;){Node<K,V>f;intn,i,fh;if(tab==null||(n=tab.length)==0)tab=initTable();// 懒初始化elseif((f=tabAt(tab,i=(n-1)&hash))==null){// CAS 尝试插入新节点if(casTabAt(tab,i,null,newNode<K,V>(hash,key,value,null)))break;}elseif((fh=f.hash)==MOVED)tab=helpTransfer(tab,f);// 协助扩容else{synchronized(f){// 锁住链表头/树根// ... 链表/红黑树插入}}}addCount(1L,binCount);returnnull;}4.3 核心优化技术
- CAS 操作:无锁化初始化、计数更新
- 细粒度锁:只锁单个桶(链表头/树根)
- 扩容协助:多线程协同完成扩容
- 计数分离:使用
LongAdder思想统计 size
4.4 迭代器特性
ConcurrentHashMap使用弱一致性迭代器,迭代过程中可以反映创建迭代器之后的部分修改,但不保证反映所有修改。
5. 性能对比测试
5.1 测试场景设计
publicclassMapPerformanceTest{privatestaticfinalintTHREAD_COUNT=10;privatestaticfinalintOPERATION_COUNT=100000;publicstaticvoidtestMap(Map<String,Integer>map,StringmapName){longstart=System.currentTimeMillis();ExecutorServiceexecutor=Executors.newFixedThreadPool(THREAD_COUNT);for(inti=0;i<THREAD_COUNT;i++){finalintthreadId=i;executor.submit(()->{for(intj=0;j<OPERATION_COUNT;j++){Stringkey="thread-"+threadId+"-key-"+j;map.put(key,j);map.get(key);}});}executor.shutdown();try{executor.awaitTermination(1,TimeUnit.HOURS);}catch(InterruptedExceptione){e.printStackTrace();}longend=System.currentTimeMillis();System.out.println(mapName+" 耗时: "+(end-start)+"ms");}publicstaticvoidmain(String[]args){// 预热testMap(newHashMap<>(),"HashMap (单线程)");// 多线程测试testMap(newHashtable<>(),"Hashtable");testMap(newConcurrentHashMap<>(),"ConcurrentHashMap");testMap(Collections.synchronizedMap(newHashMap<>()),"Collections.synchronizedMap");}}5.2 预期性能排序(高并发场景)
ConcurrentHashMap > Collections.synchronizedMap ≈ HashTable6. 使用场景建议
6.1 选择 HashMap 当:
- 单线程环境
- 需要最高性能
- 允许 null 键值
- 不需要线程安全
6.2 选择 HashTable 当:
- 维护遗留代码
- 简单的同步需求(低并发)
- 明确禁止 null 值
6.3 选择 ConcurrentHashMap 当:
- 高并发读写场景
- 需要高吞吐量
- 读多写少的场景
- 需要弱一致性迭代
7. 常见面试问题
7.1 HashMap 的扩容机制
// HashMap 扩容核心逻辑finalNode<K,V>[]resize(){Node<K,V>[]oldTab=table;intoldCap=(oldTab==null)?0:oldTab.length;intoldThr=threshold;intnewCap,newThr=0;if(oldCap>0){if(oldCap>=MAXIMUM_CAPACITY){threshold=Integer.MAX_VALUE;returnoldTab;}// 容量翻倍:newCap = oldCap << 1elseif((newCap=oldCap<<1)<MAXIMUM_CAPACITY&&oldCap>=DEFAULT_INITIAL_CAPACITY)newThr=oldThr<<1;// 阈值翻倍}// ... 其他初始化逻辑// 重新哈希所有元素if(oldTab!=null){for(intj=0;j<oldCap;++j){Node<K,V>e;if((e=oldTab[j])!=null){oldTab[j]=null;if(e.next==null)newTab[e.hash&(newCap-1)]=e;elseif(einstanceofTreeNode)((TreeNode<K,V>)e).split(this,newTab,j,oldCap);else{// 链表重哈希// JDK 1.8 优化:无需重新计算哈希Node<K,V>loHead=null,loTail=null;Node<K,V>hiHead=null,hiTail=null;Node<K,V>next;do{next=e.next;// 判断元素是否需要移动到新位置if((e.hash&oldCap)==0){// 留在原索引if(loTail==null)loHead=e;elseloTail.next=e;loTail=e;}else{// 移动到新索引(原索引+oldCap)if(hiTail==null)hiHead=e;elsehiTail.next=e;hiTail=e;}}while((e=next)!=null);// ... 设置新表}}}}returnnewTab;}7.2 ConcurrentHashMap 的 size() 方法
ConcurrentHashMap的size()方法并不完全精确,它通过baseCount和CounterCell数组来统计,采用类似LongAdder的分段计数策略,在并发更新时性能更好。
8. 最佳实践
初始化容量:预估元素数量,避免频繁扩容
// 预估 1000 个元素,负载因子 0.75intexpectedSize=1000;intinitialCapacity=(int)(expectedSize/0.75f)+1;Map<String,Object>map=newHashMap<>(initialCapacity);键对象设计:重写
hashCode()和equals()方法publicclassCustomKey{privatefinalStringid;privatefinalintversion;@OverridepublicinthashCode(){returnObjects.hash(id,version);}@Overridepublicbooleanequals(Objectobj){if(this==obj)returntrue;if(obj==null||getClass()!=obj.getClass())returnfalse;CustomKeythat=(CustomKey)obj;returnversion==that.version&&Objects.equals(id,that.id);}}并发控制:根据场景选择合适的并发容器
// 读多写少:ConcurrentHashMapMap<String,Object>cache=newConcurrentHashMap<>();// 写多读少:考虑 CopyOnWriteArrayList 等// 需要排序:ConcurrentSkipListMap
总结
HashMap、HashTable和ConcurrentHashMap各有其适用场景。在现代 Java 开发中:
- 单线程环境首选
HashMap - 低并发同步可使用
Collections.synchronizedMap(new HashMap<>()) - 高并发场景必须使用
ConcurrentHashMap - 遗留系统维护才考虑
HashTable
理解它们的底层实现差异,能够帮助我们在实际开发中做出更合适的技术选型,编写出既安全又高效的程序。