前言
在前面文章
synchronized关键字的四种加锁方式
中介绍了四种synchronized的使用和区别,但是效果都是一样的,今天我们更加深入的看一看synchronized背后的’’monitor”锁,以及和Lock的区别。
synchronized背后的’’monitor”锁
synchronized的使用非常简单,仅需要在方法或者代码块中使用synchronized就可以了,使用看起来似乎很简单的背后是Java团队背后为我们做了很多的努力来简化我们的使用。synchronized如何使用直接影响了程序的效率。
因为每一个Java对象内部都只有一个锁,如果使用synchronized使一个线程获取了某一个对象的锁之后,其它线程就无法获取这个对象的锁了,因为只有一个锁,其他线程只能等这个线程释放这个锁。
首先我们看synchronized在同步代码块中的使用背后的字节码
,源码如下:
public class TestSynchronized {
public void synMethod(Thread thread) {
synchronized (this) {
}
}
public void synMethod1(Thread thread) {
synchronized (TestSynchronized.class) {
}
}
public void synMethod2(Thread thread) {
}
}
-
这里有两个使用同步代码块加锁的方法,我们使用cmd命令切换到类
TestSynchronized
对应的目录下,使用编译命令
javac TestSynchronized.java
,这时候会出现一个编译后的字节码文件
TestSynchronized.class
,使用命令
javap -verbose TestSynchronized.class
就可以查看字节码文件 如下:
Classfile /D:/develop/androidstudio/Forward/app/src/main/java/com/oman/forward/study/TestSynchronized.class
Last modified 2020-3-29; size 614 bytes
MD5 checksum a93f484683fe2ce446f688c977d73f1a
Compiled from "TestSynchronized.java"
public class com.oman.forward.study.TestSynchronized
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#19 // java/lang/Object."<init>":()V
#2 = Class #20 // com/oman/forward/study/TestSynchronized
#3 = Class #21 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 synMethod
#9 = Utf8 (Ljava/lang/Thread;)V
#10 = Utf8 StackMapTable
#11 = Class #20 // com/oman/forward/study/TestSynchronized
#12 = Class #22 // java/lang/Thread
#13 = Class #21 // java/lang/Object
#14 = Class #23 // java/lang/Throwable
#15 = Utf8 synMethod1
#16 = Utf8 synMethod2
#17 = Utf8 SourceFile
#18 = Utf8 TestSynchronized.java
#19 = NameAndType #4:#5 // "<init>":()V
#20 = Utf8 com/oman/forward/study/TestSynchronized
#21 = Utf8 java/lang/Object
#22 = Utf8 java/lang/Thread
#23 = Utf8 java/lang/Throwable
{
public com.oman.forward.study.TestSynchronized();
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 8: 0
public void synMethod(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: aload_0
1: dup
2: astore_2
3: monitorenter
4: aload_2
5: monitorexit
6: goto 14
9: astore_3
10: aload_2
11: monitorexit
12: aload_3
13: athrow
14: return
Exception table:
from to target type
4 6 9 any
9 12 9 any
LineNumberTable:
line 11: 0
line 13: 4
line 14: 14
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 9
locals = [ class com/oman/forward/study/TestSynchronized, class java/lang/Thread, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public void synMethod1(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: ldc #2 // class com/oman/forward/study/TestSynchronized
2: dup
3: astore_2
4: monitorenter
5: aload_2
6: monitorexit
7: goto 15
10: astore_3
11: aload_2
12: monitorexit
13: aload_3
14: athrow
15: return
Exception table:
from to target type
5 7 10 any
10 13 10 any
LineNumberTable:
line 17: 0
line 19: 5
line 20: 15
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 10
locals = [ class com/oman/forward/study/TestSynchronized, class java/lang/Thread, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public void synMethod2(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 24: 0
}
SourceFile: "TestSynchronized.java"
- 我们重点关注synMethod和synMethod1,他们使用的是同步代码块加锁的方式,我们看到他们里面比synMethod2普通的方法多了两个指令,分别是monitorenter 和 monitorexit 指令,而且有两个monitorexit指令,之所以有两个monitorexit指令的原因是monitorenter只需要插入到同步代码块开始的位置,而monitoreexit需要在同步代码块正常结束和异常的位置都插入,这样就可以在异常的时候也释放锁。
- 每个对象中都有一个维护着被锁次数的计数器,monitorenter代表计数器加1,monitorexit代表计数器减1,如果计数器的值为0的话,就代表着这个对象未被线程使用。
-
monitorenter
代表开始加锁,意味着执行monitorenter的线程开始尝试获取对象的monitor的所有权,加锁分为几种情况:- 如果对象中monitor的计数器为0,代表此对象没有被使用,则执行monitorenter的线程就可以占有这个对象的monitor。
- 如果对象中monitor的计数器不为0,并且此线程已经持有此对象的monitor的话,那么monitor计数器就累计加1。
- 如果对象中的monitor计数器不为0,并且其他线程已经持有了此对象的monitor的话,那么此线程就处于BLOCKED状态,需要等待这个monitor计数器的值为0,值为0意味着这个monitor已经被释放了,那么其它等待这个monitor的线程就可以再次尝试获取monitor的所有权了。
-
monitorexit
意味着将对象中的monitor计数器减1,上面分析了,当计数器的值为0的时候意味着这个monitor已经被释放了,那么其它等待这个monitor的线程就可以再次尝试获取monitor的所有权了。
接下来我们看synchronized在同步方法中使用背后的字节码
,源码如下:
public class TestSynchronized {
public void synMethod2(Thread thread) {
}
public synchronized void synMethod3(Thread thread) {
}
public static synchronized void synMethod4(Thread thread) {
}
}
-
这里有两个使用同步方法加锁的方法,我们还是和上面的命令一样,使用cmd命令切换到类
TestSynchronized
对应的目录下,使用编译命令
javac TestSynchronized.java
,这时候会出现一个编译后的字节码文件
TestSynchronized.class
,使用命令
javap -verbose TestSynchronized.class
就可以查看字节码文件 如下:
Classfile /D:/develop/androidstudio/Forward/app/src/main/java/com/oman/forward/study/TestSynchronized.class
Last modified 2020-3-29; size 409 bytes
MD5 checksum cf108dae480c6b6d58d6cfc8bb7a9c01
Compiled from "TestSynchronized.java"
public class com.oman.forward.study.TestSynchronized
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#14 // java/lang/Object."<init>":()V
#2 = Class #15 // com/oman/forward/study/TestSynchronized
#3 = Class #16 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 synMethod2
#9 = Utf8 (Ljava/lang/Thread;)V
#10 = Utf8 synMethod3
#11 = Utf8 synMethod4
#12 = Utf8 SourceFile
#13 = Utf8 TestSynchronized.java
#14 = NameAndType #4:#5 // "<init>":()V
#15 = Utf8 com/oman/forward/study/TestSynchronized
#16 = Utf8 java/lang/Object
{
public com.oman.forward.study.TestSynchronized();
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 8: 0
public void synMethod2(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 24: 0
public synchronized void synMethod3(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 29: 0
public static synchronized void synMethod4(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 33: 0
}
SourceFile: "TestSynchronized.java"
-
我们重点关注synMethod3和synMethod4,我们看到他们两个和synMethod2的主要区别在于多了一个flag为
ACC_SYNCHRONIZED
的标记,这个标记用来标识它是同步方法的。而synMethod3和synMethod4的区别则是synMethod4比synMethod3多了一个
ACC_STATIC
的静态标识符。
小结
-
使用synchronized修饰的方法会有一个
ACC_SYNCHRONIZED
标识,当此方法在准备执行的时候,会发现此方法包含
ACC_SYNCHRONIZED
标记,那么就需要先获取monitor锁(如果是非静态的话,是对象monitor,如果修饰的是静态方法则是类monitor),获取monitor之后才能执行方法体中的内容,方法执行结束后会释放monitor。 - 使用synchronized修饰同步代码块的话,会有两个标识,monitorenter和monitorexit。monitorenter 代表开始加锁,意味着执行monitorenter的线程开始尝试获取对象的monitor的所有权,获取到monitor遇到的几种情况上面已经详细分析了,当同步代码块执行结束后,就会进入monitorexit释放锁。
- 上面分析得知synchronized如果对于方法加锁的话,因为同步的范围过于大,将会影响程序的性能,所以在编程中可以考虑使用同步代码块来替代同步方法,这样对程序的性能会有提升。
synchronized和Lock的区别
synchronized和Lock都是用来保证线程安全的,下面我们比较它们主要的相同点和不同点。
-
相同点:
-
synchronized和Lock都是用来保证线程安全的
: Java并发编程的三要素包括原子性,可见性,有序性。因为使用synchronized和Lock在同一个时间,只能有一个线程执行代码,所以就保证了原子性,可见性和有序性,也就保证了线程安全。 -
synchronized和Lock都是可重入锁
:可重入锁指的是如果线程获得了某一个对象的monitor,当再次试图获取这个对象的monitor的时候,是不需要先释放之前的monitor的,上面我们分析synchronized说到monitorenter的时候,说到了monitor计数器可以累加,说明synchronized是可重入锁,Lock也是可重入锁。
-
-
不同点:
-
灵活性不同
: lock可以使用tryLock(time)等方法,如果获取不到锁的时候,可以去做其它的事情。而synchronized只能选择等待或者异常。 -
加锁的显示和隐式:
Lock必须显示的执行加锁和解锁,为了防止发生死锁,解锁一般在finally中执行。而synchronized的加锁和解锁是Java虚拟机内部实现的,通过上面的class字节码分析,本质上也是加锁和解锁的操作,并且monitorexit有两次,所以能够保证异常释放锁,只不过这些内容在代码上是不像Lock直接体现的。 -
加锁解锁的顺序:
synchronized的加锁和解锁,是按顺序出现的,而Lock可以加多个所锁,但是解锁不用按照顺序,可以把先加的锁后解。比如下面的代码://synchronized 有顺序要求 synchronized(obj1) { synchronized(obj1) { //do something } } lock无顺序要求 lock1.lock(); lock2.lock(); // do something lock2.unlock(); lock1.unlock();
-
被占有的线程个数
:因为每个对象只有一个monitor,所以synchronized只能被一个线程占有。而lock没有这个限制。比如ReentrantReadWriteLock可以同时被多个线程持有读锁。 -
公平性:
Lock可以设置公平锁和非公平锁,比如ReentrantLock默认是不公平锁,可以通过构造方法设置公平锁,而synchronized不能设置。
-
小结
对synchronized和Lock说了这么多,如何选择呢,在我看来有以下几条建议:
-
从效率考虑:
Java在包java.util.concurrent下为我们提供了很多的并发类,如果使用的话首先考虑使用这些类(前提是足够了解这些类),比如考虑
ConcurrentHashMap等替换
HashMap
。 -
安全性上考虑:
因为synchronized使用简单,而且虚拟机内置帮我们处理了解锁的操作,就算是异常情况下也会解锁,所以从编码上来看的话,synchronized比Lock更加安全(因为Lock可能会忘记在finally中解锁)。 -
从灵活性上考虑:
Lock有更多的API可供更复杂的需求选择,如果你需要比如超时功能,可以考虑使用Lock。
版权声明:本文为oman001原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。