一、通过下面的代码的字节码反编译查看synchronized在不同地方JVM在底层是怎么实现的
public class MyTest1 {
private Object object = new Object();
public void method() {
synchronized (object) {
System.out.println("hello word");
}
}
public void method2() {
synchronized (object) {
System.out.println("======");
throw new RuntimeException();
}
}
public synchronized void method3() {
System.out.println("hello world");
}
public synchronized static void method4() {
System.out.println("===========");
}
}
使用javap -v MyTest1 进行反编译查看结果如下
Classfile /E:/workspace/learnworkspace/java_concurrency/target/classes/cn/edu/concurrency3/MyTest1.class
Last modified 2020-9-2; size 1050 bytes
MD5 checksum 7e4e0cd2eafa486d2a47499999b07735
Compiled from "MyTest1.java"
public class cn.edu.concurrency3.MyTest1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#32 // java/lang/Object."<init>":()V
#2 = Class #33 // java/lang/Object
#3 = Fieldref #12.#34 // cn/edu/concurrency3/MyTest1.object:Ljava/lang/Object;
#4 = Fieldref #35.#36 // java/lang/System.out:Ljava/io/PrintStream;
#5 = String #37 // hello word
#6 = Methodref #38.#39 // java/io/PrintStream.println:(Ljava/lang/String;)V
#7 = String #40 // ======
#8 = Class #41 // java/lang/RuntimeException
#9 = Methodref #8.#32 // java/lang/RuntimeException."<init>":()V
#10 = String #42 // hello world
#11 = String #43 // ===========
#12 = Class #44 // cn/edu/concurrency3/MyTest1
#13 = Utf8 object
#14 = Utf8 Ljava/lang/Object;
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 this
#21 = Utf8 Lcn/edu/concurrency3/MyTest1;
#22 = Utf8 method
#23 = Utf8 StackMapTable
#24 = Class #44 // cn/edu/concurrency3/MyTest1
#25 = Class #33 // java/lang/Object
#26 = Class #45 // java/lang/Throwable
#27 = Utf8 method2
#28 = Utf8 method3
#29 = Utf8 method4
#30 = Utf8 SourceFile
#31 = Utf8 MyTest1.java
#32 = NameAndType #15:#16 // "<init>":()V
#33 = Utf8 java/lang/Object
#34 = NameAndType #13:#14 // object:Ljava/lang/Object;
#35 = Class #46 // java/lang/System
#36 = NameAndType #47:#48 // out:Ljava/io/PrintStream;
#37 = Utf8 hello word
#38 = Class #49 // java/io/PrintStream
#39 = NameAndType #50:#51 // println:(Ljava/lang/String;)V
#40 = Utf8 ======
#41 = Utf8 java/lang/RuntimeException
#42 = Utf8 hello world
#43 = Utf8 ===========
#44 = Utf8 cn/edu/concurrency3/MyTest1
#45 = Utf8 java/lang/Throwable
#46 = Utf8 java/lang/System
#47 = Utf8 out
#48 = Utf8 Ljava/io/PrintStream;
#49 = Utf8 java/io/PrintStream
#50 = Utf8 println
#51 = Utf8 (Ljava/lang/String;)V
{
public cn.edu.concurrency3.MyTest1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putfield #3 // Field object:Ljava/lang/Object;
15: return
LineNumberTable:
line 39: 0
line 41: 4
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Lcn/edu/concurrency3/MyTest1;
public void method();
descriptor: ()V
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
6: monitorenter
7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #5 // String hello word
12: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: aload_1
16: monitorexit
17: goto 25
20: astore_2
21: aload_1
22: monitorexit
23: aload_2
24: athrow
25: return
Exception table:
from to target type
7 17 20 any
20 23 20 any
LineNumberTable:
line 48: 0
line 49: 7
line 50: 15
line 51: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 this Lcn/edu/concurrency3/MyTest1;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 20
locals = [ class cn/edu/concurrency3/MyTest1, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public void method2();
descriptor: ()V
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
6: monitorenter
7: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #7 // String ======
12: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: new #8 // class java/lang/RuntimeException
18: dup
19: invokespecial #9 // 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
LineNumberTable:
line 54: 0
line 55: 7
line 56: 15
line 57: 23
LocalVariableTable:
Start Length Slot Name Signature
0 28 0 this Lcn/edu/concurrency3/MyTest1;
StackMapTable: number_of_entries = 1
frame_type = 255 /* full_frame */
offset_delta = 23
locals = [ class cn/edu/concurrency3/MyTest1, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
public synchronized void method3();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #10 // String hello world
5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 66: 0
line 67: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcn/edu/concurrency3/MyTest1;
public static synchronized void method4();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #11 // String ===========
5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 73: 0
line 74: 8
}
SourceFile: "MyTest1.java"
1、通过method的方法可以看出当使用synchronized关键字来修饰代码块时,字节码层面是通过monitorenter和monitorexit指令来实现锁的获取与释放操作 当线程进入到monitorenter指令后,线程将会持有monitor对象也就是当前对的锁,退出monitorenter指令后,线程将会释放monitor对象,但是有两个monitorexit第二个monitorexit是为了防止有异常抛出,保证无论什么情况下程序执行完成都会释放掉对象的锁。
2、通过method2方法的反编译结果来看,只有一对monitorenter和monitorexit 因为synchronized代码块中抛出了异常,代码块只有一种结束方法,所以只有一个monitorexit。
3、通过method3和method4方法反编译结果可以看出,当synchronized修饰方法的时候在反编译中没有monitorenter和monitorexit指令,
而是在方法的flags里面标记一下ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
ACC_PUBLIC就代表方法是public的 ACC_STATIC 表示方法是静态方法 ACC_SYNCHRONIZED 就表示方法是同步方法。
关于synchronize关键字使用的总结
1、被synchronized修改的方法或者代码块,方法或者代码块如果执行则表示已经获取到了当前对象的锁。那么其他同一个对象被synchronized修饰的方法或者代码块就不能同时执行。
2、说明如果一个对象有多个synchronized修饰的方法,那么在同一时刻只能有一个方法执行。
3、被synchronized修改的方法或者代码块只能有一个线程可以执行,其他线程都必须等待当前线程释放掉这个对象的锁。
4、当线程访问一个被synchronized修饰static的静态方法的时候,获取到的锁并不是某个实例对象的锁,而是类对象的锁。
5、当多个线程同时访问被synchronize修饰的方法或者代码块的时候,只有一个线程可以成功获取到当前对象的锁,其他线程就会进入阻塞状态。
被阻塞线程JVM的处理方式
JVM中的同步是基于进入与退出监视器对象(管程)(monitor)来实现的,每个对象实例都会有一个monitor对象
monitor对象会和对象一同创建和销毁,monitor对象是由C++实现的
当多个线程同时访问同一段同步代码时,这些线程会被放到一个EntryList的集合中,处于阻塞阻塞状态的线程都会被放入到该列表当中,接下来,当线程获取到monitor对象的锁时,因为monitor是依赖于操作系统的mutex lock 来实现互斥的,线程获取mutex成功,
则会持有mutex,这时其他对象举无法在获取到mutex了。
还有如果在同步代码块中调用了wait方法,那么线程就会释放掉持有的mutex,并且该线程就会进入到WaitSet集合中,等待下一次其他线程调用notify或者notifyAll唤醒,如果当前线程顺利执行完毕也会释放所持有的mutex
同步锁在这个实现方式中,因为monitor是依赖于底层操作系统实现,这就存在用户态和内核态之间的切换,会增加性能开销。
通过对象互斥锁的概念来保证共享数据操作的完整性,每个对象都对应一个可称为【互斥锁】的标记,这个标记用于保证在任何时刻,只能有一个线程访问该对象
用户态和内核态的概念
当线程处于active状态时是处于用户态的,但是当线程被阻塞,线程就会被放入EntryList集合中,这时候线程就会进入内核态,当线程尝试获取锁并且成功获取后
又会从内核态切换为用户态,这样多线程频繁切换状态就会导致资源的浪费。
JVM的一种优化方式就是使用自旋锁。
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,
直到获取到锁才会退出循环。获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成 busy-waiting。但是当自旋一段时间还是不能获取到锁,也会进入到阻塞状态。
它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,
在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行线程可以获得锁。但是两者在调度机制上略有不同。对于互斥锁,
如果资源已经被占用,资源申请线程只能进入阻塞状态。但是自旋锁不会立即引起阻塞,如果锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁
自旋锁的优点
自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
自旋锁存在的缺点
如果某个线程持有锁的时间过长,然后很多线程等待这个锁就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
哪些处于EntryList和WaitSet的线程都会进入阻塞状态,阻塞状态是由操作系统来完成的,在liunx上是通过pthread_mutex_lock来实现的
线程被阻塞后便会进去内核态,这样来回切换就会影响锁的性能。