JVM同步原语Synchronized
通过synchronized
关键字,可以获取一个对象的monitor(以下统称对象锁),那么在字节码层面,synchronized关键字是如何获取对象锁的呢?
有三种方式
- 将对象包裹在synchronized语句块中
- 调用对象的被synchronized关键字修饰的方法
- 调用对象所属类的被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_SYNCHRONIZED
flag,如果有的话,就会在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
标志位来决定执行方法时是否要获取对象锁的