携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
前景回顾
上一章节我们介绍了Java中锁的概念,一起认识了锁是什么,为什么要有锁。文章直达
今天我们来看看synchronized这个玩意到底是怎么起作用的。
编译与反编译
不知道大家有没有用过java命令去编译一个class文件,如果没有,那么接下来还不认真学习一下?
主程序
先写一个类,啥也没有,就一个普通的打印“Hello World!”方法。
梦开始的地方。
public class Sync {
public void add() {
System.out.println("Hello World!");
}
}
好,写完了,干嘛呢?
编译一下咯。
有小伙伴会问,main方法都没有,能编译成功么?
当然能!main方法是程序执行入口,这个类没main方法说明不在这执行主程序罢了!
OK,我们尝试编译一下。
编译
使用javac Sync.java编译指令进行编译,生成了Sync.class文件。
反编译
怎么看Sync.class文件呢?
使用javap -c Sync.class编译指令进行反编译,输出文件信息。
看看里面是啥:
Compiled from "Sync.java"
public class com.maicim.study.Sync {
public com.maicim.study.Sync();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void add();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
可以看到里面包含了类信息,方法信息等等,不认识的可以自行百度。
是不是很简单,又学到一招,可以拿去给小白吹牛了。
synchronized
言归正传,我们想看看synchronized在字节码里是怎样的形式以及怎么起作用的。
修改主程序,方法内给对象加上synchronized关键字:
public class Sync {
public void add() {
synchronized (this) {
System.out.println("Hello World!");
}
}
}
同样的执行编译和反编译,看看结果:
Compiled from "Sync.java"
public class com.maicim.study.Sync {
public com.maicim.study.Sync();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void add();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String Hello World!
9: invokevirtual #4 // 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
在上面的结果中,我把重点部分圈出来让大家看到更加明显。
可以看到,add方法内部出现了monitorenter和monitorexit。
没错,它就是synchronized关键字在class文件中的映射。
monitorenter和monitorexit之间的指令代表锁区间。
指令一旦进入
monitorenter,代表当前线程获取到了锁,其余也想获取资源的线程全部阻塞,自动排队等待抢占锁。指令一旦退出
monitorexit,代表当前线程释放了锁,其余也想获取资源的线程开始抢占锁。
它们一般是成对出现的,那这里为啥会有两个monitorexit?
因为在程序在持有锁的过程中可能会出现异常,需要异常退出,并释放锁。
有一个这样的面试题:
持有锁的线程出现异常后,会释放锁么?
会啊,当让会,你觉得设计锁的大佬会没考虑到这一点吗?要是问为啥会,请你把我这篇文章拍到面试官的脸上。
那么是一定有两个monitorexit吗?
不一定。若是主动抛出异常,则只会有一个。
修改主程序为这样:
public class Sync {
public void add() {
synchronized (this) {
System.out.println("Hello World!");
throw new RuntimeException("error");
}
}
}
自己去看看反编译的结果是啥吧,我就不贴上来了。
只要明白这一点:
jvm必须保证锁的正常获取和释放。
总结
今天带大家学习了怎么使用命令进行编译java代码文件和反编译查看字节码,并对synchronized的底层实现有了初步了解。
认识了monitorenter和monitorexit以及它们的作用。
简单阐述了一下线程获取锁的场景。
但是我们也仅仅是了解了浅层原理而已,对于synchronized这把锁,我接下来还要继续深挖,看看它到底是怎么一回事。
一起听歌吧
《路过人间》 -- 郁可唯
快快抹干眼泪 看昙花多美
路过人间 无非一瞬间
每段并肩 都不过是擦肩
曾经辜负哪位 这才被亏欠
路过人间 一直这轮回