1. 引言
锁是 Java 并发编程的核心,也是面试中几乎必考的高频话题。面试官往往从「什么是锁」开始,层层深入问到synchronized的锁升级、AQS 原理、ReentrantLock的实现等底层细节。本文整理了一批 Java 锁相关的高频面试题,并给出由浅入深的参考答案,帮助你在面试中从容应对。
2. 基础概念类面试题
2.1 什么是悲观锁和乐观锁?Java 中分别如何实现?
- 悲观锁:假设并发冲突一定会发生,操作数据前先加锁,阻塞其他线程。Java 中的
synchronized、Lock都是悲观锁。 - 乐观锁:假设冲突很少发生,不加锁,更新时通过版本号或 CAS 校验是否被修改,冲突则重试。Java 中的
AtomicInteger、LongAdder基于 CAS 实现乐观锁。
// 乐观锁:CAS 自增AtomicIntegercount=newAtomicInteger(0);count.incrementAndGet();2.2 什么是公平锁和非公平锁?区别是什么?
- 公平锁:按线程请求锁的顺序(FIFO)依次获取,先来先服务,避免饥饿,但吞吐量较低。
- 非公平锁:线程可以「插队」直接尝试获取锁,吞吐量更高,但可能造成某些线程长期得不到锁(饥饿)。
ReentrantLock默认非公平,可通过构造参数指定:
ReentrantLockfairLock=newReentrantLock(true);// 公平锁ReentrantLockunfairLock=newReentrantLock(false);// 非公平锁(默认)2.3 什么是可重入锁?为什么需要可重入?
可重入锁(递归锁)指同一个线程可以多次获取同一把锁而不会死锁。synchronized和ReentrantLock都是可重入锁。可重入避免了同一线程在递归调用或嵌套调用时因重复加锁而自我死锁。
publicsynchronizedvoidmethodA(){methodB();// 同一线程再次获取锁,不会死锁}publicsynchronizedvoidmethodB(){}2.4 什么是共享锁和独占锁?
- 独占锁(排他锁):同一时刻只能被一个线程持有,如
synchronized、ReentrantLock、读写锁中的写锁。 - 共享锁:同一时刻可被多个线程同时持有,如
ReentrantReadWriteLock中的读锁。
面试追问
- 追问:乐观锁在并发冲突频繁时性能如何?答:冲突频繁时 CAS 会大量自旋重试,CPU 开销大,性能反而下降,此时悲观锁更合适。
- 追问:CAS 存在哪些问题?答:ABA 问题(可用版本号解决)、自旋开销大、只能保证单个变量的原子性。
- 追问:公平锁一定比非公平锁好吗?答:不一定。公平锁避免饥饿但吞吐量低;非公平锁吞吐量高但可能造成线程饥饿,需按场景权衡。
3. synchronized 高频面试题
3.1 synchronized 的三种用法分别锁的是什么?
- 修饰实例方法:锁是当前实例对象
this。 - 修饰静态方法:锁是当前类的
Class对象。 - 修饰代码块:锁是括号内指定的对象。
publicclassSyncDemo{publicsynchronizedvoidinstanceMethod(){}// 锁:thispublicstaticsynchronizedvoidstaticMethod(){}// 锁:Class 对象publicvoidblockMethod(){synchronized(this){}// 锁:指定对象}}3.2 synchronized 的锁升级过程是怎样的?
JDK 1.6 之后,synchronized引入锁升级机制,从低到高单向升级:
- 无锁:没有竞争,直接访问。
- 偏向锁:只有一个线程访问时,记录线程 ID,减少 CAS 开销。
- 轻量级锁:多个线程交替访问时,通过 CAS 自旋获取锁。
- 重量级锁:竞争激烈时,升级为依赖操作系统互斥量的重量级锁,未获取锁的线程被阻塞。
锁升级是单向的,只能从低到高,不能降级。之所以不允许降级,是因为锁状态一旦升级,其内部结构(如轻量级锁的栈帧记录、重量级锁的 Monitor 对象)已经建立,若再降级需要额外的状态转换和清理逻辑,会带来无意义的开销与实现复杂性,且收益甚微——竞争一旦激烈,短期内大概率还会再次升级,反复升降只会徒增性能损耗。
在 HotSpot 实现中,偏向锁存在**撤销(revoke)与重偏向(rebias)**机制:当第二个线程访问同一对象时,偏向锁会被撤销并升级为轻量级锁;而当偏向锁被撤销后,若该对象再次被某个线程独占访问,JVM 会通过批量重偏向(bulk rebias)让锁重新进入偏向状态,从而减少不必要的锁升级。不过随着 JDK 15 起偏向锁被默认禁用并逐步废弃,这一机制在现代 JVM 中已逐渐淡出。
3.3 synchronized 和 Lock 有什么区别?
| 对比项 | synchronized | Lock |
|---|---|---|
| 实现方式 | JVM 关键字 | Java 接口(如 ReentrantLock) |
| 锁的释放 | 自动释放 | 需手动 unlock() |
| 可中断 | 否 | 支持 lockInterruptibly() |
| 公平性 | 非公平 | 可配置公平/非公平 |
| 多个条件 | 不支持 | 支持多个 Condition |
| 性能 | 锁升级后接近 Lock | 高并发下更灵活 |
面试追问
- 追问:偏向锁在 JDK 15 之后为什么被废弃?答:偏向锁的撤销需要 STW(Stop-The-World),在竞争不激烈的现代应用收益有限,且维护成本高,JDK 15 起默认禁用并逐步废弃。
- 追问:锁升级过程中出现竞争会怎样?答:轻量级锁 CAS 自旋失败后,会膨胀升级为重量级锁,未获取锁的线程进入阻塞状态,由操作系统调度。
- 追问:synchronized 锁的是对象还是代码?答:锁的是对象(监视器 Monitor),代码只是被锁保护的临界区。
4. Lock 与 AQS 面试题
4.1 ReentrantLock 是如何实现可重入的?
ReentrantLock基于 AQS 实现,内部通过state变量记录锁的持有次数:
state == 0:锁未被持有。state > 0:锁被持有,数值表示重入次数。
获取锁时通过 CAS 将state从 0 改为 1;重入时state加 1;释放时state减 1,减到 0 才真正释放锁。
4.2 AQS 是什么?它的核心组成有哪些?
AQS(AbstractQueuedSynchronizer)是 JUC 包中锁和同步器的底层框架,ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock等都基于它实现。核心组成:
- state 变量:用
volatile修饰,表示同步状态。 - CLH 队列:一个 FIFO 双向队列,存放等待获取锁的线程。
- CAS 操作:用于原子地修改
state。
4.3 AQS 获取锁的流程是怎样的?
4.4 基于 AQS 实现的同步器有哪些?
- ReentrantLock:独占锁。
- Semaphore:信号量,控制同时访问的线程数。
- CountDownLatch:倒计时门闩,等待多个线程完成。
- CyclicBarrier:循环屏障,多个线程互相等待。
- ReentrantReadWriteLock:读写锁。
面试追问
- 追问:AQS 的 CLH 队列为什么是双向队列?答:双向队列便于在释放锁时从尾部向前查找可唤醒的后继节点,也支持取消等待时快速出队,避免单向队列的遍历开销。
- 追问:state 为什么用 volatile 修饰?答:保证多线程对 state 的修改对其他线程立即可见,配合 CAS 实现原子更新,确保锁状态的一致性。
- 追问:ReentrantLock 的公平锁和非公平锁在 AQS 中如何体现?答:公平锁在 tryAcquire 时会先检查队列中是否有前驱节点,有则排队;非公平锁直接尝试 CAS 抢占,失败才入队。
5. 读写锁与进阶工具面试题
5.1 读写锁适用于什么场景?为什么读锁是共享的?
读写锁适用于读多写少的场景,如缓存、配置信息等。读操作不会修改数据,多个线程同时读是安全的,因此读锁(共享锁)可被多个线程同时持有;而写操作会修改数据,写锁(独占锁)与读锁、写锁都互斥,保证数据一致性。
ReentrantReadWriteLockrwLock=newReentrantReadWriteLock();LockreadLock=rwLock.readLock();LockwriteLock=rwLock.writeLock();readLock.lock();try{// 读共享数据}finally{readLock.unlock();}writeLock.lock();try{// 写共享数据}finally{writeLock.unlock();}5.2 StampedLock 相比读写锁有什么优势?
StampedLock是 JDK 1.8 引入的,支持三种模式:写锁、读锁、乐观读。乐观读不阻塞写线程,在读多写少且对一致性要求不高的场景下性能更高。但乐观读需要在校验失败时升级为悲观读锁。
StampedLockstampedLock=newStampedLock();longstamp=stampedLock.tryOptimisticRead();// 乐观读// 读取数据if(!stampedLock.validate(stamp)){stamp=stampedLock.readLock();// 校验失败,升级为悲观读锁try{// 重新读取}finally{stampedLock.unlockRead(stamp);}}5.3 Condition 和 Object 的 wait/notify 有什么区别?
Condition是Lock的等待/通知机制,类似于Object的wait()/notify(),但更灵活:
- 一个
Lock可以绑定多个Condition,实现更精细的等待/通知控制。 Condition支持可中断等待和超时等待。- 必须与
Lock配合使用,在lock()之后、unlock()之前调用。
Locklock=newReentrantLock();ConditionnotEmpty=lock.newCondition();ConditionnotFull=lock.newCondition();lock.lock();try{while(queue.isEmpty()){notEmpty.await();// 等待}notFull.signalAll();// 通知}finally{lock.unlock();}面试追问
- 追问:读写锁为什么会出现写饥饿?如何解决?答:读锁可被多个线程持有,若读线程持续进入,写线程可能长期等待。可通过公平模式或
StampedLock的乐观读缓解。- 追问:StampedLock 的乐观读一定比悲观读快吗?答:不一定。乐观读省去了加锁开销,但校验失败后需重读或升级为悲观读,在写频繁场景下反而更慢。
- 追问:Condition 的 await 和 Object 的 wait 有什么区别?答:
Condition.await()支持超时和可中断,且一个 Lock 可绑定多个 Condition 实现精细控制;wait()只能配合synchronized使用。
6. 锁的性能优化面试题
6.1 如何减少锁竞争、提升并发性能?
- 尽量缩小锁的粒度,减少锁持有时间。
- 优先使用
synchronized,必要时才用Lock。 - 读多写少用读写锁或
StampedLock。 - 避免在锁内执行耗时操作(如 IO、网络请求)。
- 使用
volatile、Atomic类替代部分加锁场景。 - 使用
ConcurrentHashMap等并发容器替代手动加锁。
6.2 volatile 和 synchronized 有什么区别?
volatile保证可见性和有序性,但不保证原子性;synchronized三者都保证。volatile不能修饰方法,只能修饰变量;synchronized可修饰方法、代码块。volatile不会阻塞线程,性能开销更小;synchronized可能引起线程阻塞。
6.4 锁对比总结表
| 对比项 | synchronized | ReentrantLock | ReentrantReadWriteLock | StampedLock |
|---|---|---|---|---|
| 锁类型 | 悲观锁、独占锁 | 悲观锁、独占锁 | 读锁共享、写锁独占 | 写锁独占、读锁共享、乐观读 |
| 可重入 | 支持 | 支持 | 支持 | 不支持 |
| 公平性 | 非公平 | 可配置公平/非公平 | 可配置公平/非公平 | 非公平 |
| 中断支持 | 不支持 | 支持 lockInterruptibly() | 支持 | 不支持 |
| 条件等待 | 仅 wait/notify | 支持多个 Condition | 支持多个 Condition | 不支持 |
| 适用场景 | 简单同步、锁竞争不激烈 | 需要中断、超时、公平锁的复杂场景 | 读多写少、缓存等场景 | 读多写少且对一致性要求不高、追求极致性能 |
选型建议:默认优先使用synchronized,代码简洁且锁升级后性能接近Lock;需要可中断、超时等待、公平锁或多个条件队列时改用ReentrantLock;读多写少的场景用ReentrantReadWriteLock提升并发读吞吐;对一致性要求不高且追求极致读性能时,可考虑StampedLock的乐观读,但要注意它不可重入且不支持条件等待。
7. 总结
Java 锁面试题的核心脉络是:从锁的基本分类(乐观/悲观、公平/非公平、可重入、共享/独占),到synchronized的用法与锁升级,再到Lock接口与 AQS 底层原理,最后延伸到读写锁、StampedLock、Condition等进阶工具。面试时建议结合源码(如ReentrantLock、AQS)和实际项目经验来回答,展现深度与实战能力。