JVM同步原语Synchronized

通过synchronized关键字,可以获取一个对象的monitor(以下统称对象锁),那么在字节码层面,synchronized关键字是如何获取对象锁的呢?

有三种方式

  1. 将对象包裹在synchronized语句块中
  2. 调用对象的被synchronized关键字修饰的方法
  3. 调用对象所属类的被synchronized关键字修饰的类方法

先看第一种方式

public class SyncDemo {

    private Object object = new Object();

    public void method() {
        synchronized (object) {
            System.out.println("hello, world");
        }
    }
}

接着我们通过反编译来查看生成的字节码,命令javap -v [class]

  public void method();
    descriptor: ()V
    # ACC_PUBLIC表示方法的访问权限为public
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: getfield      #3                  // Field object:Ljava/lang/Object;
         4: dup
         5: astore_1
         # 重点!!!monitorenter指令就是synchronized关键字用于获取对象锁的字节码指令
         6: monitorenter
         # 执行完monitorenter指令后,线程就获取到了对象锁
         7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: ldc           #9                  // String hello, synchronized
        12: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        15: aload_1
        # monitorexit指令是释放对象锁的指令
        16: monitorexit
        17: goto          25
        20: astore_2
        21: aload_1
        # 这里又有一个monitorexit指令?上面不是已经释放过一次了吗?<1>
        22: monitorexit
        23: aload_2
        24: athrow
        25: return
      Exception table:
         from    to  target type
             7    17    20   any
            20    23    20   any

<1>处,为什么已经用monitorexit释放过一次对象锁,这里还需要执行一次monitorexit?

正常情况下,其实只需要一个monitorexit指令即可,当synchronized语句块中的代码正常退出时,第二个monitorexit指令是用不上的

但是,如果在执行语句块中的代码时出现了异常情况导致代码提前结束,那么第一个monitorexit指令就不会被执行,此时就需要第二个monitorexit指令,通过字节码可以看到

16: monitorexit
17: goto          25
20: astore_2
21: aload_1
22: monitorexit
23: aload_2
24: athrow
25: return

正常情况下,代码执行过monitorexit之后应该会跳转到接下来的逻辑,或者是直接返回

但是后面还有一段字节码,以athrow指令结尾,就是为了处理发生异常的情况

将代码修改一下

public class SyncDemo {

    private Object object = new Object();

    public void method() {
        synchronized (object) {
            System.out.println("hello, world");
            throw new RuntimeException();
        }
    }
}

再来看一下它的字节码

public void method();
Code:
   0: aload_0
   1: getfield      #3                  // Field object:Ljava/lang/Object;
   4: dup
   5: astore_1
   6: monitorenter
   7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
  10: ldc           #5                  // String hello, world
  12: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  15: new           #7                  // class java/lang/RuntimeException
  18: dup
  19: invokespecial #8                  // Method java/lang/RuntimeException."<init>":()V
  22: athrow
  23: astore_2
  24: aload_1
  25: monitorexit
  26: aload_2
  27: athrow
Exception table:
   from    to  target type
       7    26    23   any

可以看到,现在只剩下一个monitorexit指令了

因为在这种情况下,synchronized语句块中必定会抛出异常,那么就必定不会正常返回,所以只需要有异常情况下的对象锁释放逻辑即可

接着看第二种方式

public class SyncDemo2 {

    private int count;

    public synchronized void add() {
        count++;
    }
}

查看其字节码

# 构造方法
public com.daniel.concurrency.synchronize.SyncDemo2();
    # 无参的,无返回值的
    descriptor: ()V
    # 访问权限为public的
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/daniel/concurrency/synchronize/SyncDemo2;

  # 被synchronized关键字修饰的实例方法
  public synchronized void add();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field count:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field count:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/daniel/concurrency/synchronize/SyncDemo2;

可以看到,通过synchronized修饰的实例方法add的字节码中并没有看到获取对象锁的monitorenter指令和释放对象锁的monitorexit指令

但是看到flags部分,发现它比上面的例子中多了一个名为ACC_SYNCHRONIZED的flag

JVM在执行方法时,首先会检查方法是否有ACC_SYNCHRONIZEDflag,如果有的话,就会在JVM层面先获取到对象锁,然后执行方法,当方法执行完后,JVM又会自动释放其对象锁;如果没有的话,再去检查是否有monitorenter、monitorexit指令

然后看第三种方式

使用synchronized关键字修饰的静态方法,其实锁的是类对象,并不是实例对象

public class SyncDemo3 {

    public static synchronized void method() {
        System.out.println("hello");
    }
}

查看其字节码

public static synchronized void method();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 10: 0
        line 11: 8

与第二种方式相同的是,被synchronized关键字修饰的类方法的字节码中也没有monitorenter及monitorexit指令

与第二种方式不同的是,被synchronized关键字修饰的类方法的flags中,多出了一个flag,ACC_STATIC,表示该方法是一个静态方法,另外args_size的数量变成了0,表示该方法连this指针都没有了,也就是我们所知的,静态方法中无法调用this的原因

总结

在synchronized语句块中,synchronized能够通过monitorenter、monitorexit指令来获取和释放对象锁,在正常情况下,一个拥有synchronized语句块的方法的字节码中最起码有两个monitorexit指令,这是为了防止异常情况下锁没有被释放,在异常情况下,只会有一个monitorexit指令,因为,无论如何,都会走到异常的情况,所以也就不需要多余的monitorexit指令了

在synchronized修饰的实例方法和静态方法中,都没有monitorenter、monitorexit指令,JVM是通过判断其有没有ACC_SYNCHRONIZED标志位来决定执行方法时是否要获取对象锁的