← 返回文章

Java 锁

发布于 2026/10/19

1. 完整回答

Java 锁是并发编程中用于控制多个线程访问共享资源的一种同步机制,主要解决线程安全问题,保证共享数据操作的原子性和内存可见性。

比如多个线程同时扣减库存,如果没有同步控制,可能同时读取到相同的库存数量,最终导致超卖。通过加锁,可以让关键操作按照预期的并发规则执行。

Java 中常用的锁主要有 synchronized、ReentrantLock 和 ReentrantReadWriteLock。

synchronized 是 Java 内置的同步机制,基于 JVM 的对象监视器实现。线程进入同步代码时需要先获得对应对象的锁,执行结束后自动释放。它支持可重入,同一个线程可以重复获取已经持有的锁。

ReentrantLock 是 JUC 提供的显式锁,底层基于 AQS 实现。AQS 通过同步状态、CAS 和等待队列管理锁竞争。相比 synchronized,它支持公平锁、可中断获取锁、超时获取锁以及多个条件队列,控制方式更加灵活。

对于读多写少的场景,还可以使用读写锁。读锁允许多个线程同时读取,写锁要求独占访问,从而提高读取并发能力。

锁的一个重要特性是,释放锁与后续获取同一把锁之间存在 happens-before 关系,可以保证线程之间的数据可见性。

实际开发中,普通互斥场景通常使用 synchronized;需要更灵活的锁控制时使用 ReentrantLock;读多写少且读操作具有一定开销时,可以考虑读写锁。同时需要控制锁的粒度和持有时间,减少竞争,并避免死锁。

2. 核心原理与关键概念

2.1 核心原理

Java 锁的核心机制可以概括为:获取锁 → 访问共享资源 → 释放锁。

不同锁的差异主要体现在获取锁、处理竞争和唤醒等待线程的方式上。

  1. synchronized 的工作流程

线程进入同步代码 → 尝试获取对象监视器(Monitor)→ 获取成功后执行临界区 → 执行结束自动释放锁 → 其他线程继续竞争。

关键环节:

  • 对象监视器:Java 对象都关联一个监视器,同一时刻只能有一个线程持有对应的监视器锁。

  • 可重入:同一个线程再次进入同一把锁保护的代码,可以直接重入,JVM 会记录锁的重入次数。

  • 自动释放:同步代码正常结束或发生异常退出时,JVM 都会自动释放对应的锁。

  • 内存可见性:线程释放锁之前的写操作,对后续成功获取同一把锁的线程可见。

在字节码层面,同步代码块通过 monitorenter 和 monitorexit 实现;同步方法通过 ACC_SYNCHRONIZED 标志,由 JVM 隐式完成加锁和解锁。

  1. ReentrantLock 的工作流程

线程调用 lock() → AQS 尝试获取同步状态 → 获取成功则进入临界区 → 获取失败则进入同步等待队列 → 等待被唤醒并重新竞争锁 → 最终调用 unlock() 释放锁。

关键环节:

  • state:AQS 的同步状态。对于 ReentrantLock,0 表示未被持有,正数表示当前持有线程的重入次数。

  • CAS:通过原子操作尝试修改同步状态,保证多个线程竞争时只有符合条件的线程能获取锁。

  • FIFO 等待队列:获取锁失败的线程进入队列,在适当时机挂起,等待重新竞争。

  • 唤醒机制:锁完全释放后,AQS 会通知合适的等待线程重新尝试获取锁。

需要注意,AQS 使用 FIFO 队列管理等待线程,但不代表锁一定严格按照入队顺序分配,具体取决于公平策略。

2.2 关键概念

概念 含义
互斥锁 同一时刻只允许一个线程进入受保护的临界区
可重入锁 当前线程可以重复获取已经持有的同一把锁
公平锁 竞争时通常优先照顾等待时间较长的线程
非公平锁 允许新线程在特定情况下插队获取锁
读写锁 读操作允许共享,写操作要求独占
乐观锁 假设冲突较少,更新时通过版本校验或 CAS 检测冲突
悲观锁 假设可能发生冲突,访问共享资源前先获取锁

3. 高频面试追问

3.1 synchronized 锁的到底是什么?不同使用方式有什么区别?

synchronized 锁住的是对象对应的监视器,具体锁对象由使用方式决定。

  • 同步实例方法:锁当前实例对象,也就是 this。

  • 同步静态方法:锁当前类对应的 Class 对象。

  • 同步代码块:锁括号中指定的对象。

例如,两个线程分别调用同一个实例的同步方法,会竞争同一把锁;如果调用的是两个不同实例的同步方法,通常不会互相阻塞。

但是静态同步方法使用类锁,即使通过不同实例调用,也会竞争同一个 Class 对象的锁。

另外,类锁和实例锁属于不同锁对象,因此通常不会互相阻塞。

关键在于多个线程是否使用同一把锁保护同一份共享数据。

3.2 什么是可重入锁?为什么需要支持可重入?

可重入锁指的是一个线程已经持有某把锁时,可以再次获取同一把锁,不会因为重复加锁而阻塞自身。

例如,一个同步方法 A 内部调用另一个使用相同锁的同步方法 B。如果锁支持可重入,线程执行 B 时可以继续获取这把锁。

在 ReentrantLock 中,可重入主要通过持有线程标识和重入计数实现:

线程首次获取锁 → 设置持有线程并将计数设为 1 → 同一线程再次获取锁,计数加 1 → 每次释放计数减 1 → 计数归零后真正释放锁。

synchronized 和 ReentrantLock 都支持可重入。

可重入机制可以避免线程因重复获取自身持有的锁而陷入自我阻塞,使嵌套方法调用更加自然。

3.3 AQS 是什么?它如何实现锁的获取与释放?

AQS 全称是 AbstractQueuedSynchronizer,抽象队列同步器,是 JUC 中实现锁和线程同步工具的重要基础框架。

核心由两部分组成:同步状态 state 和 FIFO 等待队列。

以 ReentrantLock 为例:

  1. 获取锁时,首先尝试通过 CAS 修改 state。

  2. 如果锁已经被其他线程持有,当前线程进入 AQS 等待队列。

  3. 排队线程在合适时机挂起,避免持续占用 CPU。

  4. 持有锁的线程执行 unlock(),重入计数归零后释放锁。

  5. AQS 唤醒等待线程,由其重新尝试获取锁。

AQS 还支持独占模式和共享模式。ReentrantLock 使用独占模式,Semaphore、CountDownLatch 等同步工具也利用 AQS 的机制实现各自的同步语义。

AQS 的价值在于统一处理竞争排队、线程阻塞和唤醒,让不同同步工具主要关注各自的资源获取规则。

3.4 synchronized 和 ReentrantLock 有什么区别?应该如何选择?

两者都属于可重入的独占锁,都能保证临界区的互斥访问和内存可见性。

主要区别是锁控制能力。

synchronized 由 JVM 管理,进入和退出同步代码时自动加锁、解锁,使用简单,能够减少忘记释放锁的问题。

ReentrantLock 需要显式调用 lock() 和 unlock(),通常通过 try-finally 保证释放,但提供了更多能力:

  • tryLock():尝试获取锁,失败可以直接返回。

  • tryLock(timeout):允许设置等待超时时间。

  • lockInterruptibly():等待获取锁时可以响应中断。

  • 公平模式:可以配置公平锁。

  • Condition:可以创建多个条件等待队列。

实际选择时,普通同步代码优先使用 synchronized;涉及超时退出、响应中断、复杂线程协作时,使用 ReentrantLock 更合适。

两者的性能与竞争程度、临界区长度和 JDK 实现有关,不能简单认定某一种始终更快。

继续追问:为什么 ReentrantLock 必须在 finally 中释放锁?

因为 ReentrantLock 需要手动解锁。如果业务代码发生异常,没有执行 unlock(),锁就可能一直被持有,导致其他线程长期等待。使用 finally 可以保证成功获取锁之后,无论业务代码是否异常退出,都能执行相应的释放操作。

3.5 公平锁和非公平锁有什么区别?为什么默认使用非公平锁?

公平锁在存在竞争时,通常优先让等待时间较长的线程获取锁;非公平锁允许后来的线程在特定情况下直接抢占锁。

ReentrantLock 默认使用非公平锁。

公平锁获取锁时,会考虑等待队列中是否存在更早的等待线程,从而尽量减少插队。

非公平锁允许新线程在锁刚好空闲时直接竞争,减少部分等待和线程调度开销,因此通常有更高的吞吐量。

但非公平锁可能让部分线程长期得不到锁,产生饥饿问题。

公平锁可以改善等待公平性,但不能保证操作系统严格按照线程请求时间调度。

另外,ReentrantLock 的无参 tryLock() 即使在公平模式下,也可能直接获取空闲锁。

因此,大部分业务场景使用默认的非公平锁即可;只有对等待公平性有明确要求时,才考虑公平锁。

3.6 读写锁是什么?什么时候使用比较合适?

读写锁通过区分读操作和写操作提高共享数据的并发访问能力。

它的并发规则是:

  • 读锁与读锁兼容:多个线程可以同时读取。

  • 读锁与写锁互斥:读取过程中需要阻止其他线程写入。

  • 写锁与写锁互斥:同一时刻只能有一个线程执行写操作。

Java 常见实现是 ReentrantReadWriteLock。

例如,一个本地配置缓存,大量请求只读取配置,少量操作更新配置。使用普通互斥锁会让读取操作相互等待,而读写锁允许多个读线程并发执行。

不过读写锁存在管理成本。如果写操作频繁、临界区很短,读写锁未必比普通互斥锁性能更好。

因此,它更适合读多写少、读操作具有一定开销的场景。

继续追问:读写锁支持锁升级和降级吗?

ReentrantReadWriteLock 支持写锁降级为读锁:持有写锁时先获取读锁,再释放写锁,可以保证数据更新后继续受到读锁保护。

它不支持直接从读锁升级为写锁,因为多个线程可能同时持有读锁并等待升级,造成相互等待。

3.7 wait/notify 和 Condition 有什么区别?为什么等待要使用 while?

两者都用于线程之间的条件等待和通知,核心思想是:条件不满足时释放关联的锁并等待,收到通知后重新竞争锁,再检查条件。

wait()、notify() 和 notifyAll() 属于 Object 的监视器机制,需要配合 synchronized 使用。

Condition 通常配合 ReentrantLock 使用,通过 await()、signal() 和 signalAll() 实现等待通知。

两者最明显的区别是,一把 ReentrantLock 可以关联多个 Condition,将不同等待条件分开管理。

例如有界阻塞队列,可以分别建立“队列非空”和“队列未满”两个条件队列,减少无关线程被唤醒。

等待条件通常使用 while 检查,因为线程可能发生虚假唤醒,也可能在重新获得锁之前,发现条件已经被其他线程改变。

另外,notify() 和 signal() 只负责通知等待线程,不会立即释放当前持有的锁。被唤醒线程仍需要重新获得锁才能继续执行。

3.8 悲观锁、乐观锁、CAS 和 volatile 有什么关系?

悲观锁和乐观锁是处理并发冲突的两种思路。

悲观锁通常在操作共享资源之前获取锁,阻止其他线程同时修改。例如 synchronized 和 ReentrantLock。

乐观锁允许线程先尝试操作,通过 CAS 或版本号等方式检查冲突,失败后重试或执行其他处理。

CAS 即 Compare-And-Set,比较并设置。其基本过程是:

读取预期值 → 比较当前值是否等于预期值 → 相等则原子更新 → 不相等则更新失败。

例如 AtomicInteger 可以使用原子操作完成计数器自增,避免普通 count++ 在多线程下发生数据竞争。

volatile 主要提供可见性和一定的有序性保证,但单独使用无法保证 count++ 这种读、改、写复合操作的原子性。

CAS 适合简单共享状态的原子更新,但高竞争时可能产生较多重试,也存在 ABA 问题。

继续追问:CAS 的 ABA 问题是什么?

ABA 指一个值原来是 A,被其他线程修改成 B,随后又改回 A。此时 CAS 只比较当前值,可能认为数据没有发生变化。

如果业务逻辑关心中间变化过程,可以通过增加版本号解决,例如使用 AtomicStampedReference 将引用和版本信息一起进行比较更新。

3.9 什么是死锁?如何预防和排查?

死锁指多个线程互相等待对方持有的资源,导致所有相关线程都无法继续执行。

例如,线程 A 持有锁 1,等待锁 2;线程 B 持有锁 2,等待锁 1,双方形成循环等待。

经典死锁需要同时满足四个条件:

  1. 互斥条件:资源同一时刻只能被一个线程占用。

  2. 请求并保持:线程持有资源时继续请求其他资源。

  3. 不可抢占:资源不能被其他线程强制夺走。

  4. 循环等待:多个线程形成循环等待关系。

实际开发中,最常用的预防方式是统一加锁顺序。例如所有线程都先获取锁 1,再获取锁 2。

另外,还可以缩短持锁时间,避免持锁执行耗时操作,并在适合的场景下使用 tryLock(timeout) 设置超时退出机制。

排查时,可以通过 jstack 或 jcmd Thread.print -l 查看线程堆栈和锁持有情况,检查是否存在循环等待。

如果系统出现线程大量阻塞、接口超时,可以重点分析线程状态、锁竞争关系和持锁代码。

3.10 synchronized 有哪些锁优化?锁升级是固定流程吗?

synchronized 的优化目标主要是降低无竞争时的加锁成本,以及减少竞争时不必要的阻塞和线程切换。

HotSpot JVM 在不同版本中采用过偏向锁、轻量级锁、自旋和监视器膨胀等技术。

这些机制的基本思路是:

  • 轻量级锁:在竞争较少时,尽量使用成本较低的方式完成同步。

  • 自旋:在合适情况下短暂重试获取锁,减少立即挂起线程的成本。

  • 监视器膨胀:竞争无法通过轻量级机制有效处理时,使用更完整的监视器竞争与等待机制。

  • 锁消除、锁粗化:JIT 编译器通过分析减少不必要的加锁,或者合并相邻的同步区域。

需要注意,常见的“无锁 → 偏向锁 → 轻量级锁 → 重量级锁”描述主要来自较早的 HotSpot 实现。

从 JDK 15 开始,偏向锁已经默认禁用,现代 JDK 的具体锁实现也在持续演进,因此不能把这套顺序理解成所有版本都遵循的固定流程。

此外,JDK 24 通过 JEP 491 改进了虚拟线程与 synchronized 的协作,解决了此前大量因监视器同步而发生的虚拟线程固定平台线程问题。

实际开发中,锁竞争的优化通常优先从缩小临界区、减少共享状态、降低持锁时间入手,再结合运行时指标判断是否需要调整锁的实现。

4. 易混淆概念对比

4.1 synchronized 与 ReentrantLock

对比维度 synchronized ReentrantLock
实现方式 JVM 内置监视器机制 JUC,基于 AQS
可重入 支持 支持
释放锁 自动释放 显式 unlock()
公平锁 不提供公平模式配置 支持配置
等待获取锁可中断 不支持直接中断锁获取 lockInterruptibly()
超时获取锁 不支持 tryLock(timeout)
条件等待 wait/notify Condition
适用场景 普通互斥同步 复杂锁控制

4.2 synchronized、volatile 与 CAS

对比维度 synchronized volatile CAS
核心作用 临界区互斥访问 可见性、有序性 单次原子条件更新
复合操作原子性 锁保护范围内可以保证 单独使用无法保证 需要设计原子更新逻辑
是否可能阻塞线程 是 否 CAS 操作本身不会等待锁
常见场景 共享对象状态修改 状态标记 原子计数、状态更新

4.3 公平锁与非公平锁

对比维度 公平锁 非公平锁
获取顺序 优先较早等待的线程 允许插队
吞吐量 通常相对较低 通常相对较高
饥饿风险 相对较低 可能发生
典型选择 对等待公平性有要求 大多数普通业务场景

4.4 ReentrantReadWriteLock 与 StampedLock

对比维度 ReentrantReadWriteLock StampedLock
读模式 共享读锁 悲观读锁、乐观读
写模式 独占写锁 独占写锁
可重入 支持 不支持
乐观读校验 不支持 通过版本戳校验
锁转换 支持写锁降级为读锁 支持尝试转换锁模式
使用场景 一般读多写少 读操作短、写冲突少且能处理重试

StampedLock 的乐观读需要在读取后执行 validate() 校验。如果期间发生写入干扰,需要重新读取,通常会退回悲观读锁。

由于乐观读期间可能观察到不一致的中间状态,它更适合能够安全处理这种情况的数据结构和短读取逻辑。

4.5 wait、sleep 与 Condition.await

对比维度 wait() sleep() Condition.await()
所属机制 Object 监视器 Thread 线程休眠 Condition 条件等待
是否释放关联锁 释放调用对象的监视器锁 不释放已持有的锁 释放关联的 Lock
使用前是否需要持锁 是 否 是
唤醒方式 notify、notifyAll、中断等 时间到期、中断 signal、signalAll、中断等
典型场景 监视器线程协作 延迟执行 复杂条件等待

5. 总结

Java 锁主要用于解决多线程访问共享资源时的线程安全问题,通过互斥访问和内存同步保证数据操作的正确性。常用的 synchronized 基于 JVM 监视器实现,支持可重入和自动释放锁,适合一般互斥场景;ReentrantLock 基于 AQS,通过同步状态、CAS 和等待队列管理线程竞争,提供公平锁、可中断获取锁、超时获取锁等能力。对于读多写少的场景,还可以通过读写锁提升读取并发能力。实际使用时,需要根据共享资源的访问方式和竞争程度选择锁,同时控制锁粒度、缩短持锁时间,避免死锁和不必要的性能开销。