之前的文章中介绍了
JAVA
中一些并发锁使用方法以及里面的介绍。同时之后还介绍了字节码的操作码,让大家先了解下里面的指令,我这里也是从表面中去讲解下锁底层操作码的实现。
(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)
锁对象程序:
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的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取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来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
锁定关键点:
对象在内存中布局主要有:对象头,实例数据以及对齐填充。
- 实例数据:存放类的属性数据信息,包括父类的属性信息;
- 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
- 对象头:Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
Synchronized
用的锁就是存在Java对象头里的,那么什么是Java
对象头呢?Hotspot
虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)
、Class Pointer(类型指针)
。其中Class Pointer
是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word
用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
这里面我们主要注意的是Mark Word
这个存储结构。
每一个
Java
对象创建出来就带了一把看不见的锁,它叫做内部锁或者Monitor
锁。Monito
r对象存在于每个Java对象的对象头Mark Word
中(存储的指针的指向),Synchronized
锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait
等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。