实现原理

实现原理

Monitor

synchronized 关键字依赖于内部的 intrinsic lock 或者所谓的 monitor lock。每个对象都有一个与之关联的固有锁。按照惯例,线程必须在访问对象之前获取对象的监视器锁,然后在完成对它们的锁定后释放该监视器锁。据说线程在获得锁和释放锁之间拥有该锁。只要一个线程拥有监视器锁,其他任何线程都无法获得相同的锁。另一个线程在尝试获取锁时将阻塞。当线程释放锁时,将在该动作与任何随后的相同锁获取之间建立 happens-before 关系。

public class SynchronizedDemo {

  // 同步方法
  public synchronized void syncMethod() {
    System.out.println("Hello World");
  }

  // 同步代码块
  public void syncBlock() {
    synchronized (this) {
      System.out.println("Hello World");
    }
  }
}

以上的示例代码,编译后的字节码为:

public synchronized void syncMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return

  public void syncBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #5                  // class com/hollis/SynchronizedTest
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #3                  // String Hello World
        10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return

从 JVM 规范中可以看到 synchronized 在 JVM 里的实现原理,JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,而方法同步是使用另外一种方式实现的,细节在 JVM 规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。

monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处,JVM 要保证每个 monitorenter 必须有对应的 monitorexit 与之配对。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。

Java 对象头

synchronized 用的锁是存在 Java 对象头里的。如果对象是数组类型,则虚拟机用 3 个字宽(Word)存储对象头,如果对象是非数组类型,则用 2 字宽存储对象头。在 32 位虚拟机中,1 字宽等于 4 字节,即 32bit,如下图所示:

Java 对象头的长度

Java 对象头里的 Mark Word 里默认存储对象的 HashCode、分代年龄和锁标记位。32 位 JVM 的 Mark Word 的默认存储结构如下表所示。

Java 对象头的存储结构

在运行期间,Mark Word 里存储的数据会随着锁标志位的变化而变化。Mark Word 可能变化为存储以下 4 种数据:

Mark Word的状态变化

在 64 位虚拟机下,Mark Word 是 64bit 大小的,其存储结构如下所示:

Mark Word的存储结构

下一页