之前的文章中介绍了 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
锁对象,所以必须在同步代码块中使用。