并发源码面试答题思路?

访客 源码剖析 2

从源码到实战的深度拆解

目录导读

  1. 为什么并发源码是面试硬通货?
  2. 答题核心框架:五步拆解法
  3. 高频源码考点精讲:AQS、ReentrantLock、ThreadPoolExecutor
  4. 实战问答:面试官最爱的三道题
  5. 底层原理与性能陷阱
  6. 从“背源码”到“讲思想”的蜕变

为什么并发源码是面试硬通货?

Q:面试官问并发源码,到底在考察什么?
A:面试官不是为了让你背诵 lock() 方法里第几行是什么代码,而是在检验三个层次的能力:

  1. 底层理解:你是否明白锁、线程池、并发容器背后的设计哲学。
  2. 问题定位:当线上发生死锁、CPU 飙升、OOM 时,能否从源码角度快速定位根因。
  3. 拓展能力:对 volatile、CAS、AQS 的掌握,决定了你能否写出无锁、高吞吐的代码。

你以为在考源码,实际在考“并发世界观”。


答题核心框架:五步拆解法

当面试官问“请说说 ReentrantLock 的加锁流程”,不要直接扔出 sync.acquire(1),按以下五步回答:

  1. 一句话本质:先说出类的核心设计意图(如:AQS 是并发同步器框架,ReentrantLock 通过 AQS 实现了独占锁)。
  2. 关键数据结构:简述 waitStatus、state、CLH 队列变体(双向队列)。
  3. 流程图式核心逻辑:用“尝试获取→失败加入队列→自旋/挂起→唤醒”的脉络。
  4. 差异化细节:对比公平锁与非公平锁的 hasQueuedPredecessors 调用差异。
  5. 实战亮点:指出“可重入”如何通过 state >1 实现,“中断”如何用 acquireInterruptibly 处理。

Q:这样回答会不会太啰嗦?
A:面试官真正想听的是结构性思维,先给骨架再填血肉,比跳跃式背诵更显逻辑深度。


高频源码考点精讲:AQS、ReentrantLock、ThreadPoolExecutor

AbstractQueuedSynchronizer(AQS)——一切锁的基石

  • state 的三种角色:同步状态(0 空闲,1 占用),可重入计数(>1),共享模式剩余许可。
  • CLH 队列的变体:头节点虚拟节点(dummy node)、尾节点通过 CAS 插入、节点状态 waitStatus 的 SIGNAL/CANCELLED/PROPAGATE 转换。
  • 模板方法设计tryAcquire / tryRelease 留给子类实现,AQS 只负责排队、唤醒、自旋。

例题
Q:为什么 AQS 的头节点是一个虚拟节点?
A:避免队列为空的边界判断——当第一个线程入队时,需先创建一个 dummy 节点作为 head,再挂载真实节点,这样 head.next 始终指向第一个等待者,简化了唤醒逻辑。

ReentrantLock——公平锁 vs 非公平锁的“一念之差”

  • 非公平锁:加锁时直接 CAS 抢 state,抢不到才进队列。
  • 公平锁:加锁前先检查 hasQueuedPredecessors,若队列有等待者则放弃抢占。
  • 可重入currentThread == exclusiveOwnerThread 时,state 直接 +1。
  • 中断响应lockInterruptibly() 通过设置 Thread.interrupted() 检查点,抛出 InterruptedException

源码片段记忆点(只需口述逻辑无需原行):

// 非公平锁尝试加锁
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        // 注意:nextc < 0 表示重入次数溢出,抛出 Error
        setState(nextc);
        return true;
    }
    return false;
}

ThreadPoolExecutor——拒绝策略、核心线程与“饥饿”陷阱

  • 核心参数如何影响线程创建
    • corePoolSize:线程数 < coreSize → 创建新线程优先(不论队列是否空)。
    • maximumPoolSize:队列满且线程数 < maxSize → 创建新线程。
    • workQueue:有限队列(如 ArrayBlockingQueue)会触发拒绝策略;无限队列(LinkedBlockingQueue)最终线程数不超过 coreSize。
  • 拒绝策略四大家族
    • AbortPolicy(抛异常)
    • CallerRunsPolicy(调用者线程执行)
    • DiscardOldestPolicy(丢弃队首)
    • DiscardPolicy(直接丢弃)
  • 线程存活逻辑getTask() 从队列取任务时通过 allowCoreThreadTimeOut 控制是否销毁核心线程。

Q:corePoolSize=10,maximumPoolSize=20,队列容量=100,同时来了 200 个任务,会发生什么?
A:先创建 10 个核心线程执行任务;剩余 190 个进入队列;队列满时(100 个)再创建 10 个非核心线程(共 20);再来的 80 个任务触发拒绝策略(假设是 AbortPolicy 则抛异常),注意:非核心线程执行完任务后,若 keepAliveTime 内无新任务,则会销毁。


底层原理与性能陷阱

volatile 与 CAS 的协作

  • volatile 保证可见性(每次读从主存)和禁止指令重排序(写操作前插入 StoreLoad 屏障)。
  • CAS(Unsafe.compareAndSwap)本质是总线锁(多核 CPU 用缓存锁定),保证原子性。
  • ABA 问题:ThreadPoolExecutor 的 worker 状态避免 ABA 靠的是 compareAndSetState(c, c+1) 后若被改则重试。

锁的升级与降级(偏向锁→轻量级锁→重量级锁)

  • 偏向锁:只有一个线程访问同步块时,直接记录线程 ID,无需 CAS。
  • 轻量级锁:当竞争发生,升级为自旋锁(CAS 更新 mark word)。
  • 重量级锁:自旋超过阈值(如 10 次)或线程数 > CPU/2,转为操作系统互斥量,线程进入阻塞。
  • 注意:ReentrantLock 属 API 级别锁,不走此升级路径(纯依赖 AQS+CAS+挂起)。

常见性能陷阱

  • 死锁:多个锁嵌套,未按固定顺序获取(如线程 A 持有 lock1 等 lock2,线程 B 持有 lock2 等 lock1)。
  • 活锁:线程反复重试但始终失败(如 CAS 持续失败,但未退避)。
  • 伪共享(False Sharing):多线程修改同一缓存行的不同变量,导致缓存行无效化(解决方案:@Contended 注解或填充 long 变量)。

Q:如果线程池中线程数设置过大,为什么会导致性能下降?
A:CPU 上下文切换成本急剧上升(每次切换需保存寄存器、程序计数器、刷新 TLB),尤其当线程 > CPU 核数时,切换时间可能超过任务执行时间,产生线程切换饥饿


实战问答:面试官最爱的三道题

题1:请手写一个基于 AQS 的共享锁(如 Semaphore)的核心逻辑

答题思路

  • 设定 state 为许可数量;
  • tryAcquireShared:CAS 减一(>=0 返回剩余数,<0 则入队);
  • tryReleaseShared:CAS 加一,循环直到成功;
  • 关键点:释放后需唤醒后继节点(通过 setHeadAndPropagate 传播)。

题2:volatile 为什么能保证线程安全?不能保证哪些场景?

回答结构

  • 保证:可见性(写后刷新主存)、有序性(禁止重排序),如单例双重检查锁中 volatile 修饰 instance 防止指令重排。
  • 不能保证:原子性(如 i++ 需要 synchronized 或 AtomicInteger)。
  • 特例:对 volatile 变量的单个读/写是原子的(64 位 long/double 在 32 位 JVM 上需特别处理)。

题3:ThreadPoolExecutor 中 shutdown()shutdownNow() 的区别?

  • shutdown():设置 RUNNING→SHUTDOWN,中断空闲线程,但等待队列中任务继续执行。
  • shutdownNow():设置 RUNNING→STOP,尝试中断所有线程(包括正在执行的),返回队列中未执行的任务列表。
  • 源码差异interruptIdleWorkers vs interruptWorkers + drainQueue
  • 注意:正在执行的任务是否响应中断取决于是否检测 Thread.interrupted()

从“背源码”到“讲思想”的蜕变

面试官最终想看到的不是“你记得多少行源码”,而是你能否用源码思维解构问题,当你面对一个并发场景时,能自然地联想到:

  • 这个场景适合用 AQS 的独占模式还是共享模式?
  • 如果自旋代价高,是否应该先阻塞?
  • volatile 在这里能否替代锁?不能的话,还需要哪些同步原语?

真正的并发高手,是能根据 CPU 模型(MESI、缓存行)、内存模型(Java Memory Model)、操作系统的线程调度,反过来优化代码的工程师。

最后一句忠告:面试前别死记硬背,拿源码当“字典”而非“圣经”——关键节点(如 AQS 的 acquireQueued 方法)用流程图理解其自旋+阻塞组合,远比背诵行号更有价值。

标签: 并发 源码

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