多线程与高并发(一)—— 自顶向下理解Synchronized实现原理
一、 什么是锁?
在多线程中,多个线程同时对某一个资源进行访问,容易出现数据不一致问题,为保证并发安全,通常会采取线程互斥的手段对线程进行访问限制,这个互斥的手段就可以称为锁。锁的本质是状态+指针,当一个线程进入临界区前需要先修改状态,表明已加锁,并且指针指向加锁的线程。后续线程在进入临界区时同样需要尝试修改状态,修改状态前首先检查指针是否为空,如果不为空且指向其他线程则表明已经有其他线程占用了锁,则无法进行状态修改,也就是此线程获取锁失败。
二、 Synchronized 锁原理
Synchronized 关键字如何实现同步互斥?
一、生成字节码
首先了解 Synchronized 的三种用法:
- 锁对象实例
- 锁方法实例
以上三种不同的使用方式,JVM 生成的字节码也不同,具体如下:
- 锁对象实例
Synchronized(this) {
}
通过 反编译生成的字节码可看到,生成了字节码指令 monitorenter 和 monitorexit;当代码执行到monitorenter时加锁,执行monitorexit时解锁。Exception table 意为异常跳表, 如下,该异常表监测了7-13行的指令,也就是同步块,如果在同步块中出现了异常导致无法解锁,指令会跳转到 target 16 行执行,如此便能保证即使出现异常也不会导致永远无法退出锁。
public void test();
Code:
0: aload_0
1: getfield #3 // private Object lock = new Object();
4: dup
5: astore_1
6: monitorenter
7: aload_0
8: invokevirtual #4 // Method foo:()V
11: aload_1
12: monitorexit
13: goto 21
16: astore_2
17: aload_1
18: monitorexit
19: aload_2
20: athrow
21: return
Exception table:
from to target type
7 13 16 any
16 19 16 any
- 锁方法实例
synchronized public void test() {
}
方法级别的同步不会生成 monitorenter 和 monitorexit 指令,通过常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现,JVM在调用方法时,对方法的符号引用(flags)进行解析,ACC_PUBLIC 为公共方法,ACC_SYNCHRONIZED 为同步方法,如果此方法是同步方法则会进行加锁。
public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
二、字节码如何执行?
- JVM 初始化时会为每个字节码指令都创建一个模板,每个模板都关联到其对应的汇编代码生成函数。以 HotSpot jdk8 为例,该模板位于src/share/vm/interpreter/templateTable.cpp(源码地址:https://cseweb.ucsd.edu/classes/sp16/cse120-a/applications/ln/lecture9.html
[3] [oracle 官方文档]:https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html