真实面经题目 · 原创解析
java中的锁有哪些?
Java 中的锁不能只回答 synchronized 和 ReentrantLock,而要按实现机制、语义特征、等待方式、读写模型和 JVM 优化层级来拆解。面试时应从对象监视器、AQS 显式锁、读写锁、StampedLock、CAS 原子类、LockSupport 与 Condition 这些核心工具出发,再说明乐观/悲观、公平/非公平、可重入/不可重入、自旋/阻塞等分类维度,并结合适用场景、性能取舍和常见陷阱给出判断依据。
真实面经题目 · 原创解析
Java 中的锁不能只回答 synchronized 和 ReentrantLock,而要按实现机制、语义特征、等待方式、读写模型和 JVM 优化层级来拆解。面试时应从对象监视器、AQS 显式锁、读写锁、StampedLock、CAS 原子类、LockSupport 与 Condition 这些核心工具出发,再说明乐观/悲观、公平/非公平、可重入/不可重入、自旋/阻塞等分类维度,并结合适用场景、性能取舍和常见陷阱给出判断依据。
Java 中的锁可以从多个维度回答。第一类是内置锁,也就是 synchronized,它基于对象监视器实现,可以修饰实例方法、静态方法或代码块,进入临界区时自动加锁,退出或异常时自动释放,配合 wait、notify、notifyAll 完成线程协作。第二类是 JUC 中基于 AQS 的显式锁,典型代表是 ReentrantLock,它支持可重入、可中断获取锁、超时获取锁、公平锁选择以及多个 Condition 条件队列,适合需要更细控制的并发场景。第三类是读写分离锁,例如 ReentrantReadWriteLock,适用于读多写少,允许多个读线程并发,但写线程互斥。第四类是 StampedLock,它提供写锁、悲观读锁和乐观读模式,适合读多写少且读操作较短的场景,但它不可重入,使用不当容易造成死锁或校验遗漏。除此之外,还要从锁语义上区分悲观锁和乐观锁,悲观锁先互斥再访问,乐观锁先访问再通过 CAS 或版本号校验冲突;从调度上区分公平锁和非公平锁,公平锁降低饥饿风险但吞吐通常较低,非公平锁吞吐更好但可能让等待线程更久;从线程状态上区分自旋锁和阻塞锁,自旋适合临界区很短的竞争,阻塞适合等待时间较长的竞争。JVM 还曾对 synchronized 做过偏向锁、轻量级锁、重量级锁等优化概念,但需要注意偏向锁在较新 JDK 中已经默认禁用并移除,不能把它当作所有版本都稳定存在的运行时特性。实际选型时,简单互斥优先 synchronized,需要可中断、超时、多条件队列时选 ReentrantLock,读多写少可考虑 ReadWriteLock 或 StampedLock,简单状态更新优先考虑 Atomic 类和 CAS,但要注意 ABA、自旋开销、锁粒度过大、忘记释放锁以及条件队列误用等问题。
synchronized 是 Java 语言级锁,锁对象可以是实例对象、Class 对象或显式传入的对象引用。它依赖对象监视器完成互斥,进入同步块时获取 monitor,退出同步块时释放 monitor,异常退出也会自动释放,因此写法简单、可靠性高。
Object monitor 不只负责互斥,也承载 wait、notify、notifyAll 的线程协作语义。wait 必须在持有同一对象监视器时调用,会释放锁并进入等待队列;notify 只负责唤醒等待线程,线程真正继续执行还要重新竞争锁。
ReentrantLock 是显式锁,底层依赖 AQS 管理同步状态和等待队列。它相比 synchronized 提供更强控制能力,例如 lockInterruptibly、tryLock、超时获取锁、公平锁构造参数以及多个 Condition,适合复杂同步流程。
Condition 是 ReentrantLock 的条件等待机制,一个锁可以创建多个条件队列,从而把不同等待原因拆开管理。await 会释放当前锁并挂起线程,signal 或 signalAll 唤醒后还要重新获取锁,通常必须配合 while 循环校验条件。
ReadWriteLock 将访问拆成读锁和写锁,读读可以并发,读写和写写互斥。它适合读远多于写、读操作耗时明显、共享数据一致性要求高的场景;如果写很多或临界区很短,读写锁的管理成本可能抵消收益。
StampedLock 提供写锁、悲观读锁和乐观读模式。乐观读不是传统意义上的互斥锁,读取后必须通过 stamp 校验期间是否发生写入;它适合读多写少、读路径短、可以重试的场景,但不可重入,不能直接替代 ReentrantReadWriteLock。
悲观锁认为冲突经常发生,所以访问共享资源前先加互斥锁;乐观锁认为冲突较少,先执行操作,再通过 CAS、版本号或时间戳校验是否冲突。AtomicInteger、AtomicReference 等原子类就是典型 CAS 风格工具。
公平锁倾向按照等待顺序分配锁,减少线程饥饿,但需要更多排队和调度成本;非公平锁允许新来的线程直接竞争,吞吐量通常更高。ReentrantLock 可以选择公平或非公平,synchronized 没有提供公平性开关。
可重入锁允许同一线程在已经持有锁的情况下再次获取同一把锁,避免递归调用或链式调用时自我死锁。synchronized 和 ReentrantLock 都是可重入锁,而 StampedLock 不可重入,嵌套获取时需要格外谨慎。
自旋锁让线程短时间循环等待,避免立刻阻塞带来的上下文切换成本,适合锁持有时间极短的竞争;阻塞锁会挂起线程,适合等待较久的竞争。synchronized 的偏向锁、轻量级锁、重量级锁属于 JVM 优化和实现层概念,并且偏向锁在新版本 JDK 中已有重要变化。
synchronized 是语言级内置锁,自动释放,写法简单;ReentrantLock 是显式锁,支持可中断、超时、公平锁和多个 Condition,但需要手动 unlock,适合更复杂控制。
AQS 用一个 state 表示同步状态,用 FIFO 队列管理竞争失败的线程,并提供独占和共享两种获取模式。具体锁只需要定义如何尝试获取和释放状态。
因为 ReentrantLock 不会像 synchronized 一样自动释放。业务代码抛异常时如果没有在 finally 中 unlock,锁会一直被当前线程持有,其他线程可能永久阻塞。
如果写操作频繁、读操作很短、锁竞争复杂或存在锁升级需求,读写锁的状态维护和排队成本可能超过收益,还可能带来写线程饥饿问题。
乐观读期间并没有真正阻止写线程修改数据,因此读取完成后必须 validate 校验期间是否发生写入。如果校验失败,就要退化为悲观读锁或重新读取。
CAS 只比较当前值是否等于旧值,如果值从 A 变成 B 又变回 A,CAS 可能误以为没有变化。常见解决方式是增加版本号或使用带版本戳的原子引用。
不一定。公平锁能减少饥饿,但需要更严格排队和更多调度切换,吞吐通常较低。很多默认实现偏向非公平,是为了在大多数场景获得更高吞吐。