
引言在 Java 集合框架中HashMap、HashTable和ConcurrentHashMap都是基于哈希表实现的 Map 接口实现类它们在多线程环境下的表现、性能特征和适用场景有着显著差异。理解这三者的区别对于编写高效、安全的并发程序至关重要。本文将深入剖析它们的底层实现、线程安全性、性能表现以及使用场景。1. 核心特性对比概览特性HashMapHashTableConcurrentHashMap线程安全否是方法级 synchronized是分段锁/CAS允许 null 键/值是否否键值均不允许初始容量161116扩容因子0.750.750.75迭代器Fail-FastFail-FastWeakly Consistent继承体系AbstractMapDictionaryAbstractMapJava 版本1.21.01.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){NodeK,V[]tab;NodeK,Vp;intn,i;// 懒加载第一次 put 时才初始化数组if((tabtable)null||(ntab.length)0)n(tabresize()).length;// 计算索引位置(n-1) hashif((ptab[i(n-1)hash])null)tab[i]newNode(hash,key,value,null);// 直接插入else{// 处理哈希冲突// ... 链表/红黑树插入逻辑}// 检查是否需要扩容if(sizethreshold)resize();returnnull;}2.3 线程安全问题HashMap不是线程安全的在多线程环境下同时修改可能导致数据丢失多个线程同时 put 可能覆盖彼此的数据死循环JDK 1.7 及之前版本在扩容时可能形成环形链表大小不一致size 字段更新不同步// 线程不安全的示例publicclassHashMapThreadUnsafeDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{MapString,IntegermapnewHashMap();Threadt1newThread(()-{for(inti0;i1000;i){map.put(keyi,i);}});Threadt2newThread(()-{for(inti1000;i2000;i){map.put(keyi,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(valuenull){thrownewNullPointerException();}// 确保 key 不为 nullEntry?,?tab[]table;inthashkey.hashCode();intindex(hash0x7FFFFFFF)%tab.length;// ... 插入逻辑}3.2 性能瓶颈由于所有方法都是synchronized的HashTable存在严重的性能问题锁粒度粗整个对象一把锁并发度低竞争激烈多个线程无法同时读写吞吐量低高并发场景下性能急剧下降3.3 使用限制不允许null键和null值初始容量为 11质数扩容为2n1继承自Dictionary类已过时4. ConcurrentHashMap高并发优化方案4.1 演进历程JDK 1.5-1.7分段锁Segment实现JDK 1.8CAS synchronized 优化4.2 JDK 1.8 实现原理// ConcurrentHashMap 的 put 方法核心简化finalVputVal(Kkey,Vvalue,booleanonlyIfAbsent){if(keynull||valuenull)thrownewNullPointerException();inthashspread(key.hashCode());intbinCount0;for(NodeK,V[]tabtable;;){NodeK,Vf;intn,i,fh;if(tabnull||(ntab.length)0)tabinitTable();// 懒初始化elseif((ftabAt(tab,i(n-1)hash))null){// CAS 尝试插入新节点if(casTabAt(tab,i,null,newNodeK,V(hash,key,value,null)))break;}elseif((fhf.hash)MOVED)tabhelpTransfer(tab,f);// 协助扩容else{synchronized(f){// 锁住链表头/树根// ... 链表/红黑树插入}}}addCount(1L,binCount);returnnull;}4.3 核心优化技术CAS 操作无锁化初始化、计数更新细粒度锁只锁单个桶链表头/树根扩容协助多线程协同完成扩容计数分离使用LongAdder思想统计 size4.4 迭代器特性ConcurrentHashMap使用弱一致性迭代器迭代过程中可以反映创建迭代器之后的部分修改但不保证反映所有修改。5. 性能对比测试5.1 测试场景设计publicclassMapPerformanceTest{privatestaticfinalintTHREAD_COUNT10;privatestaticfinalintOPERATION_COUNT100000;publicstaticvoidtestMap(MapString,Integermap,StringmapName){longstartSystem.currentTimeMillis();ExecutorServiceexecutorExecutors.newFixedThreadPool(THREAD_COUNT);for(inti0;iTHREAD_COUNT;i){finalintthreadIdi;executor.submit(()-{for(intj0;jOPERATION_COUNT;j){Stringkeythread-threadId-key-j;map.put(key,j);map.get(key);}});}executor.shutdown();try{executor.awaitTermination(1,TimeUnit.HOURS);}catch(InterruptedExceptione){e.printStackTrace();}longendSystem.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 扩容核心逻辑finalNodeK,V[]resize(){NodeK,V[]oldTabtable;intoldCap(oldTabnull)?0:oldTab.length;intoldThrthreshold;intnewCap,newThr0;if(oldCap0){if(oldCapMAXIMUM_CAPACITY){thresholdInteger.MAX_VALUE;returnoldTab;}// 容量翻倍newCap oldCap 1elseif((newCapoldCap1)MAXIMUM_CAPACITYoldCapDEFAULT_INITIAL_CAPACITY)newThroldThr1;// 阈值翻倍}// ... 其他初始化逻辑// 重新哈希所有元素if(oldTab!null){for(intj0;joldCap;j){NodeK,Ve;if((eoldTab[j])!null){oldTab[j]null;if(e.nextnull)newTab[e.hash(newCap-1)]e;elseif(einstanceofTreeNode)((TreeNodeK,V)e).split(this,newTab,j,oldCap);else{// 链表重哈希// JDK 1.8 优化无需重新计算哈希NodeK,VloHeadnull,loTailnull;NodeK,VhiHeadnull,hiTailnull;NodeK,Vnext;do{nexte.next;// 判断元素是否需要移动到新位置if((e.hasholdCap)0){// 留在原索引if(loTailnull)loHeade;elseloTail.nexte;loTaile;}else{// 移动到新索引原索引oldCapif(hiTailnull)hiHeade;elsehiTail.nexte;hiTaile;}}while((enext)!null);// ... 设置新表}}}}returnnewTab;}7.2 ConcurrentHashMap 的 size() 方法ConcurrentHashMap的size()方法并不完全精确它通过baseCount和CounterCell数组来统计采用类似LongAdder的分段计数策略在并发更新时性能更好。8. 最佳实践初始化容量预估元素数量避免频繁扩容// 预估 1000 个元素负载因子 0.75intexpectedSize1000;intinitialCapacity(int)(expectedSize/0.75f)1;MapString,ObjectmapnewHashMap(initialCapacity);键对象设计重写hashCode()和equals()方法publicclassCustomKey{privatefinalStringid;privatefinalintversion;OverridepublicinthashCode(){returnObjects.hash(id,version);}Overridepublicbooleanequals(Objectobj){if(thisobj)returntrue;if(objnull||getClass()!obj.getClass())returnfalse;CustomKeythat(CustomKey)obj;returnversionthat.versionObjects.equals(id,that.id);}}并发控制根据场景选择合适的并发容器// 读多写少ConcurrentHashMapMapString,ObjectcachenewConcurrentHashMap();// 写多读少考虑 CopyOnWriteArrayList 等// 需要排序ConcurrentSkipListMap总结HashMap、HashTable和ConcurrentHashMap各有其适用场景。在现代 Java 开发中单线程环境首选HashMap低并发同步可使用Collections.synchronizedMap(new HashMap())高并发场景必须使用ConcurrentHashMap遗留系统维护才考虑HashTable理解它们的底层实现差异能够帮助我们在实际开发中做出更合适的技术选型编写出既安全又高效的程序。