源码剖析高频面试考点?

访客 源码剖析 1

从底层原理到实战解析,助你斩获大厂Offer

目录导读

  1. 为什么源码剖析是面试“必杀技”?
  2. 高频考点一:HashMap底层源码深度解析
    • 1 数据结构演变:数组+链表 → 红黑树
    • 2 核心方法:put()与get()的原子性操作
    • 3 扩容机制:resize()的阈值计算与死循环隐患
  3. 高频考点二:ConcurrentHashMap如何实现线程安全?
    • 1 JDK7分段锁 vs JDK8 CAS+Synchronized
    • 2 size()计数:CounterCell与LongAdder思想
  4. 高频考点三:ThreadLocal内存泄漏与弱引用
    • 1 set()与get()的Entry设计
    • 2 为什么务必调用remove()?
  5. 高频考点四:Synchronized与ReentrantLock的底层博弈
    • 1 偏向锁→轻量级锁→重量级锁的升级路径
    • 2 AQS队列与Condition的等待/通知机制
  6. 高频考点五:Spring IOC容器与Bean生命周期
    • 1 refresh()模板方法下的12个关键步骤
    • 2 三级缓存破解循环依赖的哲学
  7. 面试官追问:这些坑你踩过吗?
  8. 源码学习的三层境界

为什么源码剖析是面试“必杀技”?

在BAT、TMD等大厂面试中,“看过源码吗?”几乎是必问环节,根据对牛客网、LeetCode面经的统计,HashMap、ConcurrentHashMap、ThreadLocal、Spring IOC 四大源码模块的出现频率占技术面问题的60%以上。

面试官的真实逻辑

  • 表层:你记不记得默认容量是16?
  • 中层:为什么负载因子是0.75?
  • 底层:红黑树退化为链表的条件是什么?为什么?

你该展示的:不是背诵注释,而是设计者的权衡——比如HashMap用 hash & (n-1) 替代取模,是因为位运算更快且能充分利用低位信息。


高频考点一:HashMap底层源码深度解析

1 数据结构:数组+链表 → 红黑树

JDK8中,当链表长度≥8且数组长度≥64时,链表转为红黑树,为什么?泊松分布理论显示,理想随机hash下链表长度达到8的概率小于千万分之一,红黑树插入/删除的O(log n)比链表O(n)更稳定。

2 put()方法核心逻辑(附伪代码)

final V putVal(int hash, K key, V value, boolean onlyIfAbsent) {
    // 1. 数组为空则resize()
    // 2. 对应桶为空直接newNode
    // 3. 桶不为空:
    //    - 如果key已存在,直接替换
    //    - 如果是红黑树,调用putTreeVal
    //    - 如果是链表,遍历到尾部插入;若长度≥8,treeifyBin()
    // 4. modCount++,若size > threshold,resize()
}

高频问题:为什么重写equals()必须重写hashCode()?
:HashMap先通过hashCode定位桶,再用equals比较value,若两个对象equals相等但hashCode不同,它们会分到不同桶,导致get()返回null——这违反了Java Object的约定。

3 resize()扩容机制:为什么多线程下会出现死循环?

  • JDK7采用头插法:多线程并发put时,扩容可能导致链表形成环,下一个get()陷入无限循环。
  • JDK8改为尾插法:避免环,但仍有数据丢失风险。:并发场景请用ConcurrentHashMap。

面试追问:为什么容量始终是2的幂?
底层答案:因为 hash & (capacity-1) 等价于 hash % capacity,且位运算效率远高于取模,同时保证索引均匀分布。


高频考点二:ConcurrentHashMap如何实现线程安全?

1 JDK7 vs JDK8的设计哲学

  • JDK7: 分段锁(Segment继承ReentrantLock),默认16个Segment,理论上支持16线程并发写。
  • JDK8: 摒弃分段锁,采用 CAS + Synchronized(只锁链表的头节点或红黑树的根节点),并发粒度更细,且Synchronized经JVM优化后性能不输Lock。

2 size()计数的妙用

ConcurrentHashMap 通过 CounterCell 数组分散计数,在并发写时尝试CAS更新 baseCount;若冲突则使用 CounterCell 自增,size() 累加所有计数值,这借鉴了 LongAdder 的思想:空间换时间,减少CAS竞争


高频考点三:ThreadLocal内存泄漏原理

1 Entry设计的特殊之处

ThreadLocalMap 的Entry继承 WeakReferencekey是弱引用,value是强引用

  • ThreadLocal 对象被回收(例如置为null),弱引用key变为null,但value依然被Entry引用。
  • 若线程不结束(如线程池复用线程),该Entry便无法被回收,造成内存泄漏。

2 为什么必须调用remove()?

源码中 get()set() 会主动清理key为null的Entry,但若你只 set()remove(),下一次 get() 可能不触发清理。最安全的做法:每次使用后调用 ThreadLocal.remove(),尤其在线程池中。


高频考点四:Synchronized与ReentrantLock的底层博弈

1 锁升级过程:从偏向到重量

  • 偏向锁:无竞争时,Mark Word存储线程ID,同一线程再次进入无需CAS;
  • 轻量级锁:线程交替执行,通过CAS自旋获取锁,不阻塞;
  • 重量级锁:竞争激烈时,线程挂起进入内核态,依赖操作系统mutex。

面试官常问:为什么JDK6优化后的Synchronized不再“重”?
:因为锁升级机制让无竞争场景零开销,而JVM通过 偏向锁撤销(批量重偏向)进一步优化。

2 AQS与Condition

ReentrantLock 基于 AbstractQueuedSynchronizer(AQS),其核心是 CLH队列变体——每个等待线程通过CAS抢占状态。

  • Condition.await() 挂起线程,并释放锁;
  • signal() 将等待队列中的线程转移到同步队列,争取锁。

对比:Synchronized的 wait/notify 只能配合内置锁,而 Condition 支持多个等待队列(如生产-消费模型),且 await 可响应中断。


高频考点五:Spring IOC容器与Bean生命周期

1 refresh():IOC容器的12步模板

AbstractApplicationContext.refresh() 为例:

  1. prepareRefresh():准备环境;
  2. obtainFreshBeanFactory():加载Bean定义(XML/注解);
  3. prepareBeanFactory():配置类加载器;
  4. postProcessBeanFactory():子类扩展;
  5. 关键:调用 invokeBeanFactoryPostProcessors():扫描 @Component
  6. registerBeanPostProcessors():注册后处理器;
  7. initMessageSource():国际化;
  8. initApplicationEventMulticaster():事件广播器;
  9. onRefresh():Web容器初始化;
  10. registerListeners():注册监听器;
  11. 核心finishBeanFactoryInitialization() → 实例化所有单例Bean;
  12. finishRefresh():发布完成事件。

2 三级缓存破解循环依赖

Spring使用三级缓存解决单例属性循环依赖:

  • 一级缓存singletonObjects(已完成Bean);
  • 二级缓存earlySingletonObjects(半成品,未填充属性);
  • 三级缓存singletonFactories(ObjectFactory,生产提前暴露的Bean)。

流程演示
A依赖B,B依赖A

  1. A实例化 → 放入三级缓存;
  2. 填充属性时发现B → 创建B;
  3. B填充属性时发现A → 从三级缓存取A的ObjectFactory → 生成代理对象放入二级缓存;
  4. B完成,放入一级缓存;
  5. A继续从二级缓存拿自己,完成。

追问:为什么必须是三级缓存?二级够吗?
:若只用二级缓存,无法处理A是代理对象的情况,三级缓存中的ObjectFactory允许动态生成代理,保证提前暴露的对象能被正确代理。


面试官追问:这些坑你踩过吗?

  • Q1:HashMap的容量为什么限制为2的幂?
    A:位运算替代取模,且 (n-1) & hash 能均匀分布,同时扩容时元素要么在原位置,要么在原位置+n,效率高。

  • Q2:ConcurrentHashMap的size()是否绝对精确?
    A:不严格精确,但保证弱一致性,如果要求实时精确,建议使用 LongAddersynchronized

  • Q3:ThreadLocal与Synchronized的区别?
    A:ThreadLocal用空间换时间,每个线程一份拷贝;Synchronized用时间换空间,串行访问,适用场景:Connection、Session等非线程安全对象。

  • Q4:Spring中的Bean默认是单例,如何保证线程安全?
    A:无状态Bean(如Service)天然安全;有状态Bean需使用 @Scope("prototype") 或通过ThreadLocal/同步块处理。


源码学习的三层境界

第一层:背面试题,面试官一问就背,但经不住深挖;
第二层:看源码注释,知道为什么设计为这样;
第三层:理解设计哲学,比如HashMap用红黑树平衡读写性能,ConcurrentHashMap用CAS+Synchronized兼顾并发与简单性。

行动建议

  • 下次调试代码时,用IDE断点深入到JDK源码;
  • 尝试动手写一个简易版的HashMap(只实现put/get),体会扩容的代价;
  • 阅读Spring源码的 doCreateBean 方法,亲眼见证三级缓存的流转。

源码剖析不仅是面试的“高分武器”,更是你从“使用者”蜕变为“设计者”的必经之路。

标签: 并发

抱歉,评论功能暂时关闭!