当前位置: 首页 > news >正文

从源码角度理解Java并发编程面试题

当面试官问你“synchronized在JDK 1.6之后做了哪些优化?”你只答出偏向锁、轻量级锁、重量级锁就算及格,但要从源码层面讲清楚锁升级过程才是加分项。面试的本质不是背诵概念,而是考察你对底层实现的理解是否穿透到HotSpot虚拟机源码那一层。今天我们从源码角度解剖五道高频并发面试题,每一题都直击JUC源码的实现细节。

1. synchronized的锁升级究竟在源码里怎么标记?

很多人以为锁升级是JVM自动判断的,但具体如何标记却说不清。在HotSpot中,每个对象的对象头(Mark Word)里存储了锁状态信息。JDK 8中Mark Word的位模式如下:

无锁:0x01(偏向模式位为0)

偏向锁:线程ID + epoch + 偏向模式位1 + 锁标志位01

轻量级锁:指向栈中锁记录的指针 + 锁标志位00

重量级锁:指向监视器Monitor的指针 + 锁标志位10

关键在于锁标志位(最后2位)。当线程第一次尝试获取偏向锁时,源码中ObjectSynchronizer::fast_enter会判断对象是否可偏向。如果偏向锁撤销次数超过阈值(默认20次),JVM会禁用偏向锁,直接升级为轻量级锁。这个阈值在源码biasedLocking.hpp中定义为BiasedLockingBulkRebiasThresholdBiasedLockingBulkRevokeThreshold。轻量级锁通过CAS在栈帧中创建锁记录(Lock Record),若CAS失败则膨胀为重量级锁——调用inflate函数创建ObjectMonitor。重量级锁的等待线程会进入ObjectMonitor的_EntryList_WaitSet,这直接对应操作系统层面的mutex

2. volatile如何实现可见性?内存屏障在源码里怎么插?

volatile的可见性靠内存屏障(Memory Barrier)保证。在HotSpot源码中,volatile写操作会在指令序列后插入store-load屏障,volatile读操作会在指令序列前插入load-load屏障。源码位于orderAccess.hpporderAccess.cpp中,例如x86架构下:

inline void OrderAccess::storeload() { fence(); } inline void OrderAccess::loadload() { acquire(); }

但更为关键的是,JVM在生成字节码时会对volatile字段的访问加上ACC_VOLATILE标志,C1/C2编译器在JIT阶段根据该标志插入内存屏障。底层对应的是lock前缀指令(x86),该指令会锁住总线并刷新缓存行。面试常问:“volatile能保证原子性吗?”绝不能。源码中volatile只保证可见性和禁止指令重排序,不保证复合操作(如i++)的原子性。若需要原子性,必须配合CAS(通过Unsafe的compareAndSwapInt实现)。Unsafe类的compareAndSwapInt是native方法,最终调用Atomic::cmpxchg,底层使用lock cmpxchg指令。

3. AQS的同步队列和条件队列如何协作?源码中state的作用是关键

抽象队列同步器(AQS)是JUC锁与工具的基石。state是核心变量,被volatile修饰,用于表示资源状态(0表示无锁,1表示加锁)。当线程获取锁失败时,会加入同步队列(CLH变体)。源码acquire方法:

public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }

addWaiter将线程包装成Node节点,通过CAS插入队列尾部。队列头部的节点即拥有锁的线程。当持有锁的线程释放锁时,会唤醒后继节点(unparkSuccessor)。条件队列(Condition)与同步队列不同,它通过awaitsignal操作维护一个独立的单向链表。一个锁可以对应多个条件队列,但每个条件队列的节点最终被转移到同步队列才能竞争锁。源码await方法会创建一个Node.CONDITION节点加入条件队列,然后释放锁;signal则将条件队列头节点转移到同步队列(通过transferForSignal)。关键点:AQS的设计避免了线程粒度的频繁阻塞,只在必要时才park,极大减少了上下文切换。

4. ReentrantLock的公平锁与非公平锁在源码里只有一行CAS的差异

ReentrantLock内部通过Sync(继承AQS)实现两把锁:FairSync和NonfairSync。非公平锁的lock方法:

final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }

非公平锁一上来就尝试CAS抢占,如果成功就直接获得锁,不管同步队列里有没有等待线程。而公平锁的tryAcquire

protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } ... }

关键差异在hasQueuedPredecessors(),它检查队列中是否有比当前线程等待更久的节点。如果有,即使state为0也不抢占,老老实实排队。面试常问:“非公平锁的性能为什么更好?”因为非公平锁减少了线程唤醒的延迟,但可能导致“饥饿”。但源码显示,即使是非公平锁,当CAS失败进入acquire后,也会按照队列顺序来,所以并非完全无序。

5. ConcurrentHashMap的size()方法在JDK 1.8和1.7中实现迥异,如何保证一致性?

JDK 1.7的ConcurrentHashMap使用Segment数组,每个Segment内部维护一个HashEntry数组,size()通过遍历所有Segment并累加modCount,如果两次累加结果不一致则加锁。源码中通过sumCount()方法,使用tryLock尝试获取Segment锁,最多重试RETRIES_BEFORE_LOCK(2次)。而JDK 1.8抛弃了Segment,使用CounterCell数组分散计数竞争:

public int size() { long n = sumCount(); return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n); }

sumCount()遍历CounterCell数组并加上baseCount,同时考虑扩容时的迁移状态。在put操作时,如果CAS更新baseCount失败,就会创建CounterCell并CAS更新。size()不要求绝对精确,只能保证弱一致性,因为并发put/remove时返回的值可能是几毫秒前的快照。但get()方法同样弱一致:在1.8中get不锁,通过Unsafe.getObjectVolatile直接读Node数组,但读到的可能不是最新值。如果你需要强一致性,请使用ConcurrentHashMap的compute方法或结合lock

6. 线程池的corePoolSize和maximumPoolSize代码中是怎么判断的?

ThreadPoolExecutor的execute方法源码清晰展示了线程池工作策略:

public void execute(Runnable command) { int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); }

当工作线程数小于corePoolSize,即使其他线程空闲也会创建新线程(注意:addWorker(command, true)中第二个参数true表示按corePoolSize校验)。如果core线程数已满,则尝试加入阻塞队列;如果队列已满,则尝试创建新线程直到maximumPoolSize;如果超过maximumPoolSize则拒绝。面试高频陷阱corePoolSize=0,maximumPoolSize=较大值,且队列为SynchronousQueue时,会发生什么?每次任务都会直接尝试创建线程,因为队列无法容纳任何元素(offer立即失败),所以会直接创建新线程直到最大值,然后拒绝。源码中SynchronousQueue的offer返回false,导致走向else if (!addWorker(command, false))分支。理解ctl变量的设计也很重要,它用一个AtomicInteger的高3位表示线程池状态(RUNNING/SHUTDOWN/STOP/TIDYING/TERMINATED),低29位表示工作线程数。

7. ThreadLocal内存泄漏的根源是什么?Entry的弱引用是设计还是缺陷?

ThreadLocal的每个Thread内部有一个ThreadLocalMap,其Entry的key是弱引用(WeakReference )。源码中Entry定义:

static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }

key使用弱引用的目的是:当ThreadLocal对象不再被外部强引用时,GC可以及时回收key。但此时value仍被Entry强引用,如果线程一直存活(如线程池中的线程),value就永远不会被回收,造成内存泄漏。JDK的设计者在ThreadLocalMap的set、get、remove方法中会自动清理key为null的Entry,例如:

private int expungeStaleEntry(int staleSlot) { ... tab[i] = null; ... }

但如果不调用remove,且线程长期存活,泄漏依旧存在。真正的解决方案是:每次使用完ThreadLocal务必调用remove()。面试中经常问:“为什么不用强引用?”如果key是强引用,即使ThreadLocal对象被设为null,ThreadLocalMap中仍持有引用,导致无法回收ThreadLocal本身,泄漏更严重。弱引用+清理机制是权衡后的最佳实践

8. CAS的ABA问题在源码层面如何解决?AtomicStampedReference的底层原理

CAS(Compare And Swap)在Java中通过Unsafe类的compareAndSwapObject实现,但存在ABA问题:一个值从A变为B再变回A,CAS检查时认为没有变化。AtomicStampedReference通过额外维护一个整数时间戳来解决。核心数据结构:

private volatile Pair<V> pair; private static class Pair<T> { final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } }

每次修改时,需要同时比较reference和stamp,并且更新两者(通过compareAndSet)。源码中Unsafe的compareAndSwapObject只支持对引用对象进行CAS,所以AtomicStampedReference通过unsafe.objectFieldOffset获取Pair字段的偏移量,对整个Pair对象进行CAS。这种设计保证了“引用+版本号”的原子性。面试官常问:“AtomicMarkableReference和它有什么区别?”后者只用一个boolean标记,适合标记对象是否被修改过。

9. CountDownLatch的await()为什么能阻塞多个线程?底层只用了一个AQS共享模式

CountDownLatch的构造器传入计数N,内部Sync继承AQS并将state初始化为N。countDown()调用releaseShared(1)

public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }

tryReleaseShared通过CAS将state减1,当state变为0时触发doReleaseShared,该方法会唤醒等待队列中的所有线程。await()调用acquireSharedInterruptibly(1),内部检查tryAcquireShared返回是否小于0(即state>0)。关键点:AQS的共享模式支持多个线程同时被唤醒,而独占模式只能唤醒头节点。State的作用在CountDownLatch中非常纯粹:大于0时所有acquireShared线程阻塞,等于0时全部释放。与CyclicBarrier不同,CountDownLatch是一次性的,不能reset。

10. 活锁与死锁在源码层面如何避免?Lock提供的tryLock超时设计

死锁通常通过锁的有序获取来避免,但JUC的ReentrantLock提供了tryLock(long time, TimeUnit unit)方法,源码调用AQS的tryAcquireNanos

public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); }

doAcquireNanos会在循环中计算剩余等待时间,使用LockSupport.parkNanos精确休眠,超时后返回false。这是避免活锁和死锁的有效手段:线程不会无限等待,可以选择放弃或重试。此外,显式的锁接口支持可中断获取(lockInterruptibly),源码中通过acquireInterruptibly在park期间响应中断,从而打破死锁。面试时若提到“哲学家就餐问题”,用tryLock配合随机退避能完美解决活锁。

总结:源码视角下的并发面试题,本质是“看懂变量+理解内存模型+熟悉AQS框架”

以上的每一道题都源自真实面试,但只有从源码层面解析,才能体现你对并发机理的透彻理解。不要停留在“背答案”,要问自己:这段代码在JVM里是如何变成指令的?当你看到synchronized锁升级时,脑海中要浮现Mark Word的位分布;看到volatile,要想到X86的lock前缀;看到ReentrantLock,要浮现AQS的CLH队列在内存中如何组织。这是区分“会用”和“懂”的分水岭。从今天起,打开OpenJDK源码,逐行阅读java.util.concurrent下的核心类,你会发现面试题里所有“为什么”都有了答案。

http://www.gsyq.cn/news/1646902.html

相关文章:

  • 115proxy-for-kodi终极指南:让电视直接播放115云盘视频的完整教程
  • MySQL 基本命令操作大全(从入门到熟练)
  • PoisonSeed攻击深度解析:FIDO2未破,认证降级漏洞与防御实战
  • 携程酒店价格库存实时监控变化(python实现)
  • 5分钟解锁B站缓存视频:跨平台播放的终极解决方案
  • 《客房服务》 电视剧|在线观看|下载|完整版
  • 防止 GPT5.5 密钥封号限流中转技巧
  • SpringBoot 整合支付宝沙箱支付——从接入到回调
  • Codux:一个开源的多项目 AI 终端,Codex、Claude Code 按项目统一管理——聊聊设计思路
  • 基于YOLOv8的斗地主手牌检测实战指南
  • Spring Boot项目JUnit 5与Spring-test版本冲突诊断与解决方案
  • Innovus 23.1 电源规划实战:5步完成多电压域 Power Mesh 设计(附 IR Drop 分析)
  • 用Python开发一个简单的Web应用:步骤详解
  • RGB-D 抓取检测实战:基于 YOLOv8 与 FastSAM 的 3D 物体分割 Pipeline 实现
  • 从零搭建SpringBoot项目,这些配置细节值得留意
  • 测试转大模型:AI 测试工程师的能力跃迁,把学习路线落到项目证据
  • WorkshopDL终极指南:跨平台畅玩Steam创意工坊模组的完整解决方案
  • OFDM信道估计实战:LS与LMMSE算法在5G NR导频下的MSE对比
  • ISP CCM 色彩校正矩阵实战:3x3矩阵参数解析与24色卡ΔE优化
  • Agent 核心原理:工具调用记忆与任务规划,用业务场景检验技术取舍
  • PyTorch 图像识别数据工程:3步自动化爬取与清洗,构建专属数据集
  • PowerToys Text Extractor:解锁屏幕文字的智能钥匙,让每一段文字都触手可及
  • 舵机 PD 控制参数整定指南:从 P 项振荡到 D 项抑制的 4 步调参法
  • 工业镜头选型实战:5步公式从视野/靶面/物距计算焦距(附Excel工具)
  • RMFD 口罩人脸数据集实战:基于 ArcFace 的 95% 识别率模型复现指南
  • 心脏冠脉VR重建实战:基于3D Slicer的CTA数据分割与优势型可视化
  • YOLOv8 训练自定义 COCO 数据集:从 0 到 1 的 5 个关键步骤与性能调优
  • AI新纪元来了:普通人必须懂的3个真相
  • 如何3分钟搞定全网热门资源?专业级多平台下载神器实战指南
  • Agent 观测性:一次失败要能追到每个工具调用