从HashMap到ConcurrentHashMap:一场互联网大厂Java面试的深度源码剖析

面试场景:HashMap与ConcurrentHashMap的终极对决

面试官:今天我们来聊聊Java集合框架。张小明,你能简单说一下HashMap的实现原理吗?
张小明(挠头):啊这…就是键值对嘛!用数组+链表存数据,算个hash码找位置,冲突了就挂链表!
Kevin(微笑补充):准确来说,JDK1.8后是数组+链表+红黑树。当链表长度超过8且数组容量≥64时转红黑树,查询复杂度从O(n)降到O(log n)。核心是通过扰动函数(h = key.hashCode()) ^ (h >>> 16)减少哈希碰撞。


第一轮:HashMap的put流程

面试官:详细说说put过程?
张小明:先算hash…然后…呃…(卡壳)
Kevin

  1. 计算key的hash值,通过(n-1) & hash确定桶下标
  2. 若桶为空则直接插入Node
  3. 若发生哈希冲突:
    • 链表:尾插法(JDK1.7头插法会死循环)
    • 红黑树:按树规则插入
  4. 超过阈值(默认0.75)触发扩容,2倍扩容+rehash

第二轮:HashMap线程安全问题

面试官:多线程下HashMap为何会死循环?
张小明:因为…大家抢着改数据?
Kevin:JDK1.7扩容时头插法会导致链表逆序,多线程并发transfer可能形成环形链表,get时CPU飙升至100%。解决方案是改用ConcurrentHashMap。


第三轮:ConcurrentHashMap分段锁设计

面试官:ConcurrentHashMap如何保证线程安全?
Kevin

  • JDK1.7:Segment分段锁(默认16段),每段独立ReentrantLock,不同段可并发写
  • JDK1.8:抛弃分段锁,改用CAS+synchronized锁单个Node,配合volatile保证可见性

第四轮:CAS与synchronized优化

面试官:1.8为何改用synchronized?不怕性能差吗?
Kevin:JVM对synchronized做了大量优化(偏向锁/轻量级锁/锁消除),在低竞争场景下性能接近CAS。而CAS存在ABA问题和自旋开销。


第五轮:size()的统计难题

面试官:如何统计元素总数?
Kevin

  • 基础计数:volatile修饰的baseCount
  • 高并发补偿:CounterCell数组分段计数,最终结果=baseCount+∑CounterCell[i].value

第六轮:扩容的协作机制

面试官:多线程如何协作扩容?
Kevin

  1. 线程触发扩容时创建nextTable(2倍大小)
  2. 通过transferIndex分配迁移任务区间
  3. 其他线程检测到扩容状态时,会协助迁移数据(helpTransfer)

第七轮:红黑树的线程安全维护

面试官:树化过程如何保证线程安全?
Kevin:TreeBin内部维护lockState状态位,通过CAS控制树结构调整。查询时读锁(WAITER位),修改时写锁。


第八轮:JDK1.7与1.8的性能对比

Kevin:1.8在低并发场景吞吐量提升30%,高并发下减少内存消耗,但极端情况可能退化为Hashtable。


第九轮:实际业务场景应用

面试官:电商库存扣减如何选择?
Kevin

  • HashMap:单机缓存(需二次校验)
  • ConcurrentHashMap:本地库存分片(如秒杀系统二级缓存)
  • Redis:分布式库存

第十轮:设计思想总结

Kevin

  1. 空间换时间:负载因子权衡空间利用率与哈希冲突
  2. 降低锁粒度:从全局锁→分段锁→节点锁
  3. 无锁化尝试:优先CAS,失败再阻塞

面试官:感谢二位!结果会在一周内通知~


技术解析附录(含源码细节)

(此处详细展开每个问题的技术实现,示例:)

HashMap的扰动函数

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

你可能感兴趣的:(java,面试,开发语言)