疾风知劲草,板荡识诚臣,
勇夫安识义,智者必怀仁.
无论是开发还是面试,作为自认为不再是菜鸟的我们,在并发编程中,我们会理所当然地认为:
synchronized关键字所修饰的区域应该是越小越好.(synchronized是互斥锁,即重量级锁,相对开销 会比较大)
有兴趣的读者,可以参看本人的这篇博客: 关于synchronized的面试题
能坚持读到这里的朋友,此时心中肯定会有疑问:
莫非我错了?
这里暂且不论对错,先继续往下看…
1. synchronized方法
/**
* @Author Bertking
*/
public class SyncDemo {
int count;
// 注意这里synchronized是修饰方法的
public synchronized void add(){
count ++;
}
}
通过编译之后,查看字节码(ByteCode):
输出内容如下:
Classfile /home/mi/IdeaProjects/jols/src/constant_pool/SyncDemo.class
Last modified 2020-8-6; size 293 bytes
MD5 checksum 7e99f0f7d34ed583b75e92303126db7a
Compiled from "SyncDemo.java"
public class constant_pool.SyncDemo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#15 // constant_pool/SyncDemo.count:I
#3 = Class #16 // constant_pool/SyncDemo
#4 = Class #17 // java/lang/Object
#5 = Utf8 count
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 add
#12 = Utf8 SourceFile
#13 = Utf8 SyncDemo.java
#14 = NameAndType #7:#8 // "<init>":()V
#15 = NameAndType #5:#6 // count:I
#16 = Utf8 constant_pool/SyncDemo
#17 = Utf8 java/lang/Object
{
int count;
descriptor: I
flags:
public constant_pool.SyncDemo();
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 synchronized void add();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED// 注意这里的ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field count:I
5: iconst_1
6: iadd
7: putfield #2 // Field count:I
10: return
LineNumberTable:
line 11: 0
line 12: 10
}
SourceFile: "SyncDemo.java"
2. synchronized 代码块
/**
* @Author Bertking
*/
public class SyncDemo {
int count;
public void add(){
// synchronized修饰代码块
synchronized (this){
count ++;
}
}
}
字节码如下:
Classfile /home/mi/IdeaProjects/jols/src/constant_pool/SyncDemo.class
Last modified 2020-8-6; size 405 bytes
MD5 checksum 189776096818983fb3e874b2204e0fd3
Compiled from "SyncDemo.java"
public class constant_pool.SyncDemo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // constant_pool/SyncDemo.count:I
#3 = Class #20 // constant_pool/SyncDemo
#4 = Class #21 // java/lang/Object
#5 = Utf8 count
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 add
#12 = Utf8 StackMapTable
#13 = Class #20 // constant_pool/SyncDemo
#14 = Class #21 // java/lang/Object
#15 = Class #22 // java/lang/Throwable
#16 = Utf8 SourceFile
#17 = Utf8 SyncDemo.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // count:I
#20 = Utf8 constant_pool/SyncDemo
#21 = Utf8 java/lang/Object
#22 = Utf8 java/lang/Throwable
{
int count;
descriptor: I
flags:
public constant_pool.SyncDemo();
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 add();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter // monitorenter
4: aload_0
5: dup
6: getfield #2 // Field count:I
9: iconst_1
10: iadd
11: putfield #2 // Field count:I
14: aload_1
15: monitorexit. // monitorenter
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
4 16 19 any
19 22 19 any
LineNumberTable:
line 11: 0
line 12: 4
line 13: 14
line 14: 24
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class constant_pool/SyncDemo, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
SourceFile: "SyncDemo.java"
我们会看到同样的add方法经过编译器编译后的代码量相差较大,且实现不一样。
3. 对比
synchronized方法的字节码中方法访问标识中有ACC_SYNCHRONIZED,JVM也是根据该标志直接判断是否需要同步的。
synchronized代码块的字节码中是用**监视器(monitor)**来实现同步的,且方法中含有多次出栈入栈操作。
显然,synchronized方法 比 synchronized代码块 的VM执行效率要高。根据官方的数据,大概是快13%左右。
但是我们不能就因此否定之前的逻辑(synchronized修饰的区域越小越好)。这个需要我们自己去衡量其中的利弊。
4.总结
正如我们上面所看到的,对于synchronized方法被编译后,其methods的实现和普通方法一样,在VM的字节码层面并没有任何特别的指令来实现synchronized方法,仅仅是在methods的access_flags中多了一个ACC_SYNCHRONIZED,告知VM这是个同步方法.
而对于synchronized代码块将会在同步块的入口位置和出口位置分别插入monitorenter和monitorexit字节码指令.
写在最后,理解Java ByteCode的重要性不言而喻。本人也是在开发Java多年之后,又深入研究了The Java® Virtual Machine Specification.才感觉自己算是真正走进了Java的世界。本人考虑后期开个公众号,来专门把Java底层的东西给写出来。