Java中的锁 | Sychronized 原理
1. 反编译的monitor
Synchronized的实现依赖于JVM指令monitorenter
和monitorexit
。
如下代码中:
public class MonitorDemo {
synchronized void syncMethod(){
}
void method(){
synchronized (this){
}
}
}
使用javac编译成class文件,再使用javap -verbose
生成JVM汇编指令,如下图:
同步方法中Synchronized流程:
JVM 会给其加上
ACC_SYNCHRONIZED
标识,当线程执行一个方法前,会先检查方法是否存在ACC_SYNCHRONIZED
标识,如果存在则要去竞争对应的monitor锁,竞争锁成功再执行方法,否则线程阻塞。
同步代码块中Synchronized流程:
JVM则会在代码块开始前后插入
monitorenter
和monitorexit
指令,分别为竞争锁和释放锁。
1.1 monitorenter
在Java中,每个对象都会与一个monitor相关联,当某个monitor被拥有之后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor。
因为Synchronized锁是可以重入的,所以每个monitor都维护了一个计数器。
- 线程每次执行
monitorenter
前都会进行判断,如果当前线程拥有monitor,指令计数器就会加1; - 如果monitor说明没有获得锁,线程阻塞。
1.2 monitorexit
- 线程每执行完一次
monitorexit
,计数器就减 1,当计数器减至 0 时,monitor将会被释放,其他线程可以来竞争。
2.monitor构成及原理
2.1 对象的构成
首先我们要了解下可以被锁住的对象的构成是什么样的
锁的是对象,而非代码
- 修饰实例方法时,锁的是实例对象。
- 修饰静态方法时,锁的是类的class对象。
- 修饰代码块时,锁的是给定对象。
在Java中,对象除了自身的实例数据外,还有开发者看不到的一些数据:对象头、对齐字节。如下图:
当线程成功竞争到锁时,会修改对象头中的Mark Word
数据:偏向线程ID和锁标记。
补充点: 在Java中,任何对象都有对象头信息,这意味着任何对象都可以当锁。
基本数据类型不是对象,没有对象头信息,这也就解释了:为什么基本数据类型不能作为锁对象?
2.2 Synchronized实现原理
也就是monitor的工作原理
首先看一下monitor大概的内存模型:
它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
- Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
- Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
- OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
- Owner:当前已经获取到所资源的线程被称为Owner;
- !Owner:当前释放锁的线程。