一、参考答案

1、互斥锁: synchronized提供了一种锁机制,能够确保同一时刻只有一个线程执行某段代码。

2、对象锁和类锁: 可以锁定对象实例(方法或代码块)或整个类(静态方法)。

3、内存可见性: 保证了锁内操作对其他线程的可见性。

4、锁升级: 在JVM中,synchronized可能经历偏向锁、轻量级锁和重量级锁的升级。

二、实现原理

synchronized 是 Java 中最基本也是最经典的同步机制,它保证了在多线程环境下,共享资源的互斥访问和内存可见性。理解它的实现原理,是掌握 Java 并发编程的关键一步。

2.1 核心概念

在深入原理之前,我们先明确 synchronized 的几个核心作用:

  • 互斥性 (Mutual Exclusion):确保同一时刻只有一个线程能执行同步代码块或同步方法。
  • 可见性 (Visibility):当一个线程退出同步块时,它对共享变量的修改对其他进入该同步块的线程是可见的, 这是通过 “happens-before” 规则保证的。
  • 原子性 (Atomicity):同步块或方法内的操作被当作一个不可分割的整体执行。

synchronized 可以用在以下地方:

  • 同步方法:public synchronized void test() {}
  • 同步代码块:synchronized(this) {} 或 synchronized(ClassName.class) {}

2.2 JVM层面实现: 字节码与监视器锁 (Monitor)

synchronized 的实现依赖于 JVM 内部的 监视器锁(Monitor) 机制。我们从字节码层面来分析。
✅ 1. 同步代码块(synchronized(this))
当你编写一个同步代码块时,编译器会在字节码中插入 monitorenter 和 monitorexit 两条指令。

public void syncBlock() {
 synchronized (this) {
     // 临界区代码
     System.out.println("Inside synchronized block");
 }
}

对应的字节码(简化)

// ...
aload_0           // 将 this 引用压入操作数栈
monitorenter      // 获取 this 对象的监视器锁
// ...
getstatic         // ...
invokevirtual     // 调用 System.out.println
// ...
monitorexit       // 释放监视器锁

➡️ 2. 工作流程

  • 线程执行到 monitorenter 指令时,会尝试获取该指令参数(即 this 对象)所关联的 Monitor。
  • 如果获取成功,线程将持有该 Monitor,继续执行临界区代码。
  • 如果获取失败(说明该 Monitor 已被其他线程持有),当前线程会被阻塞,并进入该 Monitor 的 Entry Set (入口集) 队列中等待。
  • 当持有 Monitor 的线程执行到 monitorexit 指令时,它会释放 Monitor。此时,JVM 会从 Entry Set 中唤醒一个等待的线程,让它尝试获取 Monitor。

注意:为了保证即使在临界区代码抛出异常时锁也能被释放,编译器会自动生成一个 finally 块,将 monitorexit 指令放在里面。所以你会在字节码中看到不止一个 monitorexit。

2.3 同步方法 (public synchronized void syncMethod())

同步方法的实现略有不同,它没有 monitorenter 和 monitorexit 指令。

public synchronized void syncMethod() {
    // 临界区代码
    System.out.println("Inside synchronized method");
}

对应的字节码(简化):

public synchronized void syncMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED  // <--- 关键在这里
    Code:
      stack=2, locals=1, args_size=1
         // ...
         getstatic     // ...
         invokevirtual // 调用 System.out.println
         // ...

2.4 工作原理

当一个方法被 synchronized 关键字修饰时,JVM 会在方法的 方法表(MethodInfo) 中设置 ACC_SYNCHRONIZED 标志。当线程调用这个方法时,JVM 会检查该标志:

  • 如果设置了该标志,线程必须先获取与 方法所属对象(对于实例方法是 this,对于静态方法是类的 - Class 对象)相关联的 Monitor。
  • 获取成功后,执行方法体。
  • 方法执行完毕(无论是正常返回还是异常退出),JVM 会自动释放 Monitor。

无论是同步代码块还是同步方法,其本质都是通过获取和释放 Monitor 来实现同步。同步代码块是显式地在字节码中插入指令,而同步方法是通过方法表中的标志由 JVM 隐式处理。

三、Monitor的内部结构

Monitor 是一个由 JVM 内部实现的对象,你可以把它想象成一个同步工具或锁对象。它的内部结构大致如下:

  • Owner (所有者):指向当前持有该 Monitor 的线程。同一时刻,一个 Monitor 只能有一个 Owner。
  • Entry Set (入口集):存放等待获取 Monitor 锁而被阻塞的线程队列。当 Owner 释放锁时,会从这里唤醒一个线程。
  • Wait Set (等待集):存放调用了 wait() 方法而被阻塞的线程队列。这些线程已经持有了锁,但是因为某些条件不满足而主动释放锁,进入等待状态。当其他线程调用 notify() 或 notifyAll() 时,会从这里唤醒一个或所有线程,它们会重新进入 Entry Set 尝试获取锁。

一个形象的比喻:

  • Monitor = 一个会议室。
  • Owner = 正在会议室里开会的人。
  • Entry Set = 在会议室门口排队等待进去开会的人。
  • Wait Set = 开会开到一半,出去休息一下(等待某个信号),等信号来了再回来继续开会的人。他们- 休息时,会议室可以让给别人用。

四、锁的优化:从重量级锁到轻量级锁

在 JDK 1.6 之前,synchronized 的实现完全依赖于操作系统的 互斥量(Mutex),这是一种重量级锁。每次线程的阻塞和唤醒都需要从用户态切换到内核态,开销非常大。

为了提升性能,JDK 1.6 及以后版本对 synchronized 进行了一系列优化,引入了偏向锁和轻量级锁,使得 synchronized 在低竞争场景下的性能有了质的飞跃。

这些锁的实现都依赖于对象头中的 Mark Word.

4.1 对象头

  • 在 JVM 中,每个对象在内存中都有一个对象头,它包含了对象的元数据信息。synchronized 锁的状态就存储在对象头的 Mark Word 部分。
  • Mark Word 的结构是动态变化的,它会根据对象的状态(无锁、偏向锁、轻量级锁、重量级锁)来存储不同的信息

Mark Word (32 位 JVM 示例):
在这里插入图片描述

4.2 总结

synchronized 的实现原理可以概括为以下几点:

  1. 核心是 Monitor:synchronized 的同步语义是通过 JVM 内部的 监视器锁(Monitor) 来实现的。
  2. 字节码层面:
    • 同步代码块通过 monitorenter 和 monitorexit 指令来获取和释放 Monitor。
    • 同步方法通过方法表中的 ACC_SYNCHRONIZED 标志,由 JVM 隐式处理 Monitor 的获取和释放。
  3. 对象头的作用:锁的状态(无锁、偏向锁、轻量级锁、重量级锁)存储在对象头的 Mark Word 中。
  4. 锁的优化:为了提升性能,JDK 1.6 引入了偏向锁和轻量级锁。锁会根据竞争情况自动升级,从无锁到偏向锁,再到轻量级锁,最后到重量级锁,以在不同场景下达到最佳性能。
  5. 内存可见性:synchronized 除了保证互斥性,还通过 “happens-before” 规则保证了同步块内外的内存可见性。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐