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

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),核心流程如下:

  1. 监视器锁(ObjectMonitor)
    每个 Java 对象在 JVM 中都会关联一个 ObjectMonitor(监视器),其内部维护两个队列:

    • EntryList:等待获取锁的线程队列;
    • WaitSet:调用 wait() 后阻塞的线程队列。
      线程通过 monitorenter 指令尝试获取 Monitor(成功则持有锁,失败则进入 EntryList 阻塞),通过 monitorexit 指令释放 Monitor(释放后唤醒 EntryList 中的线程竞争锁)。
  2. 对象头(Mark Word)
    Java 对象的对象头(占 8 字节,64 位 JVM)中,Mark Word 字段存储了对象的锁状态信息,格式随锁状态动态变化:

    锁状态Mark Word 存储内容
    无锁对象哈希码 + 分代年龄 + 锁标志位(01)
    偏向锁持有锁的线程ID + 分代年龄 + 锁标志位(01)
    轻量级锁指向线程栈中锁记录的指针 + 锁标志位(00)
    重量级锁指向 ObjectMonitor 的指针 + 锁标志位(10)
  3. 锁升级过程(不可逆)
    JVM 为优化性能,会根据竞争激烈程度自动升级锁,流程为:
    无锁 → 偏向锁 → 轻量级锁 → 重量级锁

    • 偏向锁:单线程重复获取锁时,仅记录线程 ID,无需 CAS 操作(减少开销),适用于无竞争场景。
    • 轻量级锁:当其他线程尝试获取锁时,偏向锁升级为轻量级锁,线程通过 CAS 自旋(循环尝试)获取锁,适用于低竞争场景(避免线程阻塞的开销)。
    • 重量级锁:当自旋次数超限(默认 10 次)或竞争线程过多(超过 CPU 核心数一半),轻量级锁升级为重量级锁,依赖操作系统互斥量(Mutex)实现,线程会阻塞等待(适用于高并发场景)。

2. 为什么调用 wait()/notify() 必须在 synchronized 锁内?

wait()/notify()Object 类的方法,其底层依赖对象的 Monitor 锁,若脱离锁调用会抛出 IllegalMonitorStateException,核心原因:

  1. 依赖 Monitor 队列

    • wait():需先释放当前持有的 Monitor 锁,再将线程放入 WaitSet 阻塞;
    • notify():需从 WaitSet 中唤醒线程,将其移回 EntryList 重新竞争 Monitor 锁。
      若未持有锁,无法操作 Monitor 的队列,逻辑上不成立。
  2. 保证线程安全
    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. synchronizedReentrantLock 的区别

ReentrantLock 是 JUC 包提供的可重入锁,与 synchronized 核心差异在实现层面功能灵活性

特性synchronizedReentrantLock
实现层面JVM 层面(字节码指令 monitorenter/monitorexitAPI 层面(基于 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. synchronizedvolatile 的区别

volatile 是轻量级同步关键字,仅保证可见性和有序性,与 synchronized 差异显著:

特性synchronizedvolatile
原子性支持是(保证复合操作原子性,如 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. 如何避免死锁?

死锁的核心原因是:多个线程持有对方需要的锁,且互相等待不释放。避免死锁需从“破坏死锁必要条件”入手,常见方案:

  1. 固定锁获取顺序
    所有线程按统一顺序获取多个锁(如按对象哈希码从小到大),避免交叉等待。
    代码示例

    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);}
    }
    
  2. 减少锁粒度
    将大锁拆分为多个小锁,降低锁竞争(如 ConcurrentHashMap 的分段锁机制)。

  3. 使用超时机制
    通过 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 锁实现的线程间通信机制,核心逻辑:

  1. 线程调用 wait() 时,会先释放持有的 Monitor 锁,然后进入 WaitSet 队列阻塞(此时线程不再参与锁竞争);
  2. WaitSet 中的线程不会主动唤醒,必须通过其他线程调用同一锁对象的 notify()/notifyAll() 才能被唤醒;
  3. 被唤醒的线程会从 WaitSet 移回 EntryList 队列,重新参与 Monitor 锁的竞争,竞争成功后才能继续执行 wait() 之后的代码。
    若没有 notify()WaitSet 中的线程会永久阻塞,导致“线程泄漏”。

2. 锁升级的触发条件是什么?

锁升级是 JVM 基于“竞争程度”的动态优化,触发条件如下:

  1. 无锁 → 偏向锁
    当线程第一次获取锁时,JVM 会在 Mark Word 中记录该线程的 ID,后续该线程再次获取锁时,无需 CAS 操作,直接通过线程 ID 验证即可(适用于单线程重复加锁场景)。

  2. 偏向锁 → 轻量级锁
    当其他线程尝试获取该锁时,JVM 会撤销偏向锁(因为偏向锁仅允许一个线程持有),并将锁升级为轻量级锁。此时线程通过 CAS 自旋(循环尝试修改 Mark Word)获取锁,避免阻塞。

  3. 轻量级锁 → 重量级锁
    当满足以下任一条件时,轻量级锁升级为重量级锁:

    • 线程自旋次数超过阈值(JVM 默认 10 次,可通过 -XX:PreBlockSpin 配置);
    • 竞争线程数超过 CPU 核心数的一半(自旋会浪费 CPU 资源,此时阻塞更高效);
    • 线程在自旋过程中,其他线程又尝试获取锁(竞争加剧)。

💡 总结

synchronized 是 Java 并发编程的“基石”,核心需掌握:

  1. 使用方式与锁对象:明确实例方法、静态方法、代码块对应的锁对象,避免锁失效;
  2. 底层原理:Monitor 机制、Mark Word 结构、锁升级流程(无锁→偏向→轻量→重量);
  3. 对比差异:与 ReentrantLock(功能灵活性)、volatile(原子性支持)的核心区别;
  4. 实践避坑:选择不可变锁对象、固定锁顺序避免死锁,结合业务场景选择同步机制(简单场景用 synchronized,复杂场景用 ReentrantLock)。

面试中常结合“生产者-消费者模型”“死锁排查”等场景提问,需将原理与实践结合,展示对并发安全的深度理解。

http://www.dtcms.com/a/399026.html

相关文章:

  • 微信企业号可以做微网站吗查看wordpress访问记录
  • 企业建站程序哪个好asp简单网站开发
  • 法术光环释义
  • todesk远程到被控Mac后不显示画面
  • 上网行为安全(2)
  • 网站颜色搭配技巧网站建设征税标准
  • 虚拟主机建网站网站建设技术主管
  • Transformer原理学习(4)注意力机制
  • Linux epoll 事件机制深度解析
  • 仿制网站软件王烨名字含义
  • 网站建设教程 乐视网冠辰网站建设
  • 网站建设方案说明微信里的小程序怎么删除
  • vue <img 图片标签 图片引入
  • 防伪网站怎么做为什么打开网址都是seo综合查询
  • 做极速赛车网站怎么做网站视频
  • DP4363远程无钥匙进入(PKE)技术:便利与安全的完美融合
  • 手机网站页面长沙网页设计培训班哪家好
  • 用 Codebuddy Code CLI 快速开发中小学数学测试系统
  • 开源 java android app 开发(十四)绘图定义控件--波形显示器
  • seo网站设计费用网站过期会怎样解决
  • 网站机房建设目的温州机械网站建设
  • WLB公司内推|招联金融2026届校招|18薪
  • 平安产险深圳分公司在深圳莲花山公园 参与2025年金融教育宣传周启动仪式活动
  • 从root用户切换到某个普通用户突然报错“su: failed to execute /bin/bash: 资源暂时不可用”
  • 沧州网站建设制作设计优化浏览器提醒 WordPress
  • 网站风格什么意思快速优化系统
  • 陕西创程教育:点亮职业人生的明灯
  • AR智能巡检:工业培训的效率的革新
  • 栈实现队列方法与优化
  • 设计模式-建造者模式详解