之前的文章中介绍了 JAVA 中一些并发锁使用方法以及里面的介绍。同时之后还介绍了字节码的操作码,让大家先了解下里面的指令,我这里也是从表面中去讲解下锁底层操作码的实现。
package com.montos.detail;
public class SynchronizedDemo {
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
demo.demo();
}
public void demo() {
synchronized (this) {
System.out.println("this is demo");
}
}
}
复制代码
对其反编译:
public class com.montos.detail.SynchronizedDemo {
public com.montos.detail.SynchronizedDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/montos/detail/SynchronizedDemo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method demo:()V
12: return
public void demo();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #6 // String this is demo
9: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
}
复制代码
查看上面反编译的结果,我们可以看到反编译里面是存在 monitorenter 以及 monitorexit 的操作码,这两个操作码的作用就是:
monitorenter :每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下: monitorexit :执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。 从而达到线程之间的串行执行,同时我可以看到里面有两次 monitorexit 操作码:第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;这上面锁住的就是this。
public class SynchronizedDemo {
public synchronized void method() {
System.out.println("this is demo");
}
}
复制代码
反编译:
public com.montos.detail.SynchronizedDemo();
descriptor: ()V
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 3: 0
public synchronized void method();
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 this is demo
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
复制代码
通过上面反编译,我们发现没有之前的两个操作码了,多出来的是有标识 ACC_SYNCHRONIZED ,这里其实也是通过上面两个操作码完成的。这个方法也只是比普通的方法在常量池中多了 ACC_SYNCHRONIZED 字段。
当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
上面的两种操作本质上没有区别,只是方法的同步是一种隐式方式操作的,两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
对象在内存中布局主要有:对象头,实例数据以及对齐填充。
Java
Synchronized 用的锁就是存在 Java 对象头里的,那么什么是 Java 对象头呢?Hotspot虚拟机的对象头主要包括两部分数据: Mark Word (标记字段)、 Class Pointer (类型指针)。其中 Class Pointer 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例, Mark Word 用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
这里面我们主要注意的是 Mark Word 这个存储结构。
每一个 Java 对象创建出来就带了一把看不见的锁,它叫做内部锁或者 Monitor锁 。 Monitor 对象存在于每个 Java 对象的对象头 Mark Word 中(存储的指针的指向), Synchronized 锁便是通过这种方式获取锁的,也是为什么 Java 中任意对象可以作为锁的原因,同时 notify/notifyAll/wait 等方法会使用到 Monitor 锁对象,所以必须在同步代码块中使用。