Java `synchronized` 关键字高频面试题(原理+场景+底层实现)
Java synchronized
关键字高频面试题(原理+场景+底层实现)
🔍 一、基本概念与作用
1. synchronized
的核心作用是什么?
synchronized
是 Java 内置的线程同步关键字,用于解决多线程环境下共享资源的竞争问题,核心保证三大特性:
- 互斥性(原子性):确保同一时间只有一个线程能执行被保护的代码块/方法,避免并发修改导致的数据不一致。
- 可见性:线程释放锁时,会将工作内存中的变量刷新到主内存;其他线程获取锁时,会从主内存重新加载变量,避免“脏读”。
- 有序性:通过“锁规则”禁止指令重排序(即被
synchronized
保护的代码块会按顺序执行),避免因重排序导致的逻辑混乱。
2. synchronized
的三种使用方式及锁对象
不同使用方式对应不同的锁对象,直接决定同步范围,是面试高频考点:
修饰方式 | 锁对象 | 作用范围 | 适用场景 |
---|---|---|---|
实例方法 | 当前实例对象(this ) | 同一实例的所有同步实例方法互斥,不同实例不互斥 | 实例级共享资源(如对象的成员变量) |
静态方法 | 类的 Class 对象 | 所有实例的同步静态方法互斥(全局唯一锁) | 类级共享资源(如静态成员变量) |
代码块 | 括号内显式指定的对象 | 仅同步代码块,灵活控制锁范围 | 局部同步需求(如仅保护某段逻辑) |
代码示例(三种使用方式):
public class SynchronizedDemo {// 1. 修饰实例方法:锁对象 = 当前实例(this)public synchronized void instanceMethod() {System.out.println("实例方法同步:锁对象是 this");try {Thread.sleep(100);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}// 2. 修饰静态方法:锁对象 = SynchronizedDemo.classpublic static synchronized void staticMethod() {System.out.println("静态方法同步:锁对象是 SynchronizedDemo.class");try {Thread.sleep(100);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}// 3. 修饰代码块:锁对象 = 显式指定的 lockObjprivate final Object lockObj = new Object(); // 推荐用 final 避免锁对象被修改public void codeBlockMethod() {synchronized (lockObj) { // 锁对象 = lockObjSystem.out.println("代码块同步:锁对象是 lockObj");try {Thread.sleep(100);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}
}
🛠️ 二、底层实现与锁升级
1. synchronized
的底层实现原理
synchronized
的底层依赖 JVM 层面的监视器锁(Monitor) 和 对象头(Mark Word),核心流程如下:
-
监视器锁(ObjectMonitor):
每个 Java 对象在 JVM 中都会关联一个ObjectMonitor
(监视器),其内部维护两个队列:EntryList
:等待获取锁的线程队列;WaitSet
:调用wait()
后阻塞的线程队列。
线程通过monitorenter
指令尝试获取 Monitor(成功则持有锁,失败则进入EntryList
阻塞),通过monitorexit
指令释放 Monitor(释放后唤醒EntryList
中的线程竞争锁)。
-
对象头(Mark Word):
Java 对象的对象头(占 8 字节,64 位 JVM)中,Mark Word
字段存储了对象的锁状态信息,格式随锁状态动态变化:锁状态 Mark Word 存储内容 无锁 对象哈希码 + 分代年龄 + 锁标志位(01) 偏向锁 持有锁的线程ID + 分代年龄 + 锁标志位(01) 轻量级锁 指向线程栈中锁记录的指针 + 锁标志位(00) 重量级锁 指向 ObjectMonitor 的指针 + 锁标志位(10) -
锁升级过程(不可逆):
JVM 为优化性能,会根据竞争激烈程度自动升级锁,流程为:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
- 偏向锁:单线程重复获取锁时,仅记录线程 ID,无需 CAS 操作(减少开销),适用于无竞争场景。
- 轻量级锁:当其他线程尝试获取锁时,偏向锁升级为轻量级锁,线程通过 CAS 自旋(循环尝试)获取锁,适用于低竞争场景(避免线程阻塞的开销)。
- 重量级锁:当自旋次数超限(默认 10 次)或竞争线程过多(超过 CPU 核心数一半),轻量级锁升级为重量级锁,依赖操作系统互斥量(Mutex)实现,线程会阻塞等待(适用于高并发场景)。
2. 为什么调用 wait()
/notify()
必须在 synchronized
锁内?
wait()
/notify()
是 Object
类的方法,其底层依赖对象的 Monitor 锁,若脱离锁调用会抛出 IllegalMonitorStateException
,核心原因:
-
依赖 Monitor 队列:
wait()
:需先释放当前持有的 Monitor 锁,再将线程放入WaitSet
阻塞;notify()
:需从WaitSet
中唤醒线程,将其移回EntryList
重新竞争 Monitor 锁。
若未持有锁,无法操作 Monitor 的队列,逻辑上不成立。
-
保证线程安全:
wait()
/notify()
通常用于线程间通信(如“生产者-消费者”模型),需确保“检查条件”和“等待/唤醒”的原子性。例如:// 正确示例:wait()/notify() 在 synchronized 内调用 private final Object lock = new Object(); private boolean dataReady = false;public void consumer() {synchronized (lock) {// 1. 检查条件(需在锁内,避免条件被其他线程修改)while (!dataReady) { try {lock.wait(); // 释放锁,进入 WaitSet 等待} catch (InterruptedException e) {Thread.currentThread().interrupt();}}// 2. 消费数据(确保只有一个线程执行)System.out.println("消费数据");dataReady = false;lock.notify(); // 唤醒 WaitSet 中的生产者线程} }public void producer() {synchronized (lock) {while (dataReady) {try {lock.wait(); // 数据未消费,等待} catch (InterruptedException e) {Thread.currentThread().interrupt();}}// 生产数据System.out.println("生产数据");dataReady = true;lock.notify(); // 唤醒 WaitSet 中的消费者线程} }
若
wait()
不在锁内,步骤 1 的“检查条件”和步骤 2 的“等待”可能被其他线程打断,导致逻辑错误(如“虚假唤醒”)。
🆚 三、与其他同步机制的对比
1. synchronized
与 ReentrantLock
的区别
ReentrantLock
是 JUC 包提供的可重入锁,与 synchronized
核心差异在实现层面和功能灵活性:
特性 | synchronized | ReentrantLock |
---|---|---|
实现层面 | JVM 层面(字节码指令 monitorenter /monitorexit ) | API 层面(基于 AQS 框架实现) |
锁释放方式 | 自动释放(方法结束/异常抛出时) | 手动释放(必须调用 unlock() ,建议在 finally 中) |
公平性支持 | 仅非公平锁(无法配置) | 支持公平锁/非公平锁(构造函数传入 true 为公平锁) |
中断响应 | 不支持(线程阻塞后无法被中断) | 支持(lockInterruptibly() 可响应中断) |
条件变量 | 单一等待队列(依赖 wait() /notify() ) | 多个 Condition 队列(可精确唤醒线程) |
超时获取锁 | 不支持 | 支持(tryLock(long timeout, TimeUnit unit) ) |
适用场景 | 简单同步逻辑(如普通方法/代码块) | 复杂并发控制(如超时、中断、精确唤醒) |
代码示例(ReentrantLock 用法):
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockDemo {// 创建公平锁(传入 true)private final ReentrantLock lock = new ReentrantLock(true);// 创建两个 Condition 队列(精确唤醒)private final Condition producerCond = lock.newCondition();private final Condition consumerCond = lock.newCondition();private boolean dataReady = false;public void producer() {lock.lock(); // 加锁try {while (dataReady) {producerCond.await(); // 生产者进入指定 Condition 等待}System.out.println("生产数据");dataReady = true;consumerCond.signal(); // 仅唤醒消费者 Condition 中的线程} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock(); // 手动释放锁(必须在 finally 中,避免锁泄漏)}}public void consumer() {lock.lock();try {while (dataReady) {consumerCond.await();}System.out.println("消费数据");dataReady = false;producerCond.signal();} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();}}
}
2. synchronized
与 volatile
的区别
volatile
是轻量级同步关键字,仅保证可见性和有序性,与 synchronized
差异显著:
特性 | synchronized | volatile |
---|---|---|
原子性支持 | 是(保证复合操作原子性,如 i++ ) | 否(仅保证单次读写原子性,复合操作需额外处理) |
可见性支持 | 是(释放锁刷新主内存,获取锁加载主内存) | 是(强制变量读写直接操作主内存,禁止工作内存缓存) |
有序性支持 | 是(通过锁规则禁止重排序) | 是(通过插入内存屏障禁止重排序) |
锁机制 | 互斥锁(可能导致线程阻塞) | 无锁(仅内存语义,不阻塞线程) |
适用场景 | 多线程共享资源修改(如计数器、状态更新) | 单写多读场景(如状态标志、配置参数) |
代码示例(volatile 局限性):
public class VolatileDemo {private volatile int count = 0; // volatile 修饰计数器// 问题:count++ 是复合操作(读→改→写),volatile 无法保证原子性public void increment() {count++; // 多线程并发调用时,结果会小于预期(如 1000 线程各调用 1 次,结果可能是 998)}public static void main(String[] args) throws InterruptedException {VolatileDemo demo = new VolatileDemo();Thread[] threads = new Thread[1000];for (int i = 0; i < 1000; i++) {threads[i] = new Thread(demo::increment);threads[i].start();}for (Thread t : threads) {t.join();}System.out.println("count = " + demo.count); // 结果可能 < 1000}
}
若需保证 count++
原子性,需改用 synchronized
或原子类(如 AtomicInteger
)。
⚠️ 四、常见陷阱与优化
1. 锁对象选择不当导致线程不安全
核心陷阱:使用“可变对象”或“自动装箱对象”作为锁,可能导致锁对象被意外修改,进而锁失效。
错误示例(用 Integer 作为锁对象):
public class BadLockDemo {// 错误:Integer 是不可变类,count++ 会创建新对象,导致锁对象变化private Integer count = 0;public void increment() {synchronized (count) { // 每次 count++ 后,count 指向新对象,锁失效count++; // 多线程并发时,多个线程可能同时进入同步块}}
}
原因:Integer
是不可变类,count++
本质是 count = Integer.valueOf(count + 1)
,会创建新的 Integer
对象,导致每次进入同步块时,锁对象是不同的,同步失效。
优化建议:
- 使用
final
修饰锁对象,避免锁对象被修改; - 优先使用私有锁对象(如
private final Object lock = new Object()
),避免this
或类对象被外部共享。
正确示例:
public class GoodLockDemo {// 正确:final 修饰私有锁对象,不会被修改private final Object lock = new Object();private int count = 0;public void increment() {synchronized (lock) { // 锁对象始终是同一个,同步有效count++;}}
}
2. 如何避免死锁?
死锁的核心原因是:多个线程持有对方需要的锁,且互相等待不释放。避免死锁需从“破坏死锁必要条件”入手,常见方案:
-
固定锁获取顺序:
所有线程按统一顺序获取多个锁(如按对象哈希码从小到大),避免交叉等待。
代码示例:public class AvoidDeadLockDemo {private final Object lockA = new Object();private final Object lockB = new Object();// 固定锁顺序:先获取 hash 小的锁,再获取 hash 大的锁private void acquireLocks(Object lock1, Object lock2) {if (lock1.hashCode() > lock2.hashCode()) {Object temp = lock1;lock1 = lock2;lock2 = temp;}synchronized (lock1) { // 先锁 hash 小的synchronized (lock2) { // 再锁 hash 大的System.out.println("成功获取两个锁,执行逻辑");}}}// 线程1:获取 lockA → lockBpublic void thread1Logic() {acquireLocks(lockA, lockB);}// 线程2:获取 lockB → lockA(但被 acquireLocks 统一为 lockA → lockB)public void thread2Logic() {acquireLocks(lockB, lockA);} }
-
减少锁粒度:
将大锁拆分为多个小锁,降低锁竞争(如ConcurrentHashMap
的分段锁机制)。 -
使用超时机制:
通过ReentrantLock.tryLock()
尝试获取锁,超时则放弃并释放已持有的锁,避免无限等待。
代码示例:public void tryLockWithTimeout() {ReentrantLock lock1 = new ReentrantLock();ReentrantLock lock2 = new ReentrantLock();boolean locked1 = false;boolean locked2 = false;try {// 尝试获取 lock1,超时 1 秒locked1 = lock1.tryLock(1, TimeUnit.SECONDS);if (locked1) {// 尝试获取 lock2,超时 1 秒locked2 = lock2.tryLock(1, TimeUnit.SECONDS);if (locked2) {System.out.println("成功获取两个锁");} else {System.out.println("获取 lock2 超时,释放 lock1");}} else {System.out.println("获取 lock1 超时");}} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {// 释放已获取的锁if (locked2) lock2.unlock();if (locked1) lock1.unlock();} }
🧠 五、高频原理题
1. 为什么 wait()
后需要 notify()
唤醒?
wait()
和 notify()
是配合 Monitor 锁实现的线程间通信机制,核心逻辑:
- 线程调用
wait()
时,会先释放持有的 Monitor 锁,然后进入WaitSet
队列阻塞(此时线程不再参与锁竞争); WaitSet
中的线程不会主动唤醒,必须通过其他线程调用同一锁对象的notify()
/notifyAll()
才能被唤醒;- 被唤醒的线程会从
WaitSet
移回EntryList
队列,重新参与 Monitor 锁的竞争,竞争成功后才能继续执行wait()
之后的代码。
若没有notify()
,WaitSet
中的线程会永久阻塞,导致“线程泄漏”。
2. 锁升级的触发条件是什么?
锁升级是 JVM 基于“竞争程度”的动态优化,触发条件如下:
-
无锁 → 偏向锁:
当线程第一次获取锁时,JVM 会在Mark Word
中记录该线程的 ID,后续该线程再次获取锁时,无需 CAS 操作,直接通过线程 ID 验证即可(适用于单线程重复加锁场景)。 -
偏向锁 → 轻量级锁:
当其他线程尝试获取该锁时,JVM 会撤销偏向锁(因为偏向锁仅允许一个线程持有),并将锁升级为轻量级锁。此时线程通过 CAS 自旋(循环尝试修改Mark Word
)获取锁,避免阻塞。 -
轻量级锁 → 重量级锁:
当满足以下任一条件时,轻量级锁升级为重量级锁:- 线程自旋次数超过阈值(JVM 默认 10 次,可通过
-XX:PreBlockSpin
配置); - 竞争线程数超过 CPU 核心数的一半(自旋会浪费 CPU 资源,此时阻塞更高效);
- 线程在自旋过程中,其他线程又尝试获取锁(竞争加剧)。
- 线程自旋次数超过阈值(JVM 默认 10 次,可通过
💡 总结
synchronized
是 Java 并发编程的“基石”,核心需掌握:
- 使用方式与锁对象:明确实例方法、静态方法、代码块对应的锁对象,避免锁失效;
- 底层原理:Monitor 机制、Mark Word 结构、锁升级流程(无锁→偏向→轻量→重量);
- 对比差异:与
ReentrantLock
(功能灵活性)、volatile
(原子性支持)的核心区别; - 实践避坑:选择不可变锁对象、固定锁顺序避免死锁,结合业务场景选择同步机制(简单场景用
synchronized
,复杂场景用ReentrantLock
)。
面试中常结合“生产者-消费者模型”“死锁排查”等场景提问,需将原理与实践结合,展示对并发安全的深度理解。