学习来源:
黑马程序员全面深入学习Java并发编程,JUC并发编程全套教程_哔哩哔哩_bilibili
另外参考:
Java6及以上版本对synchronized的优化 - 蜗牛大师 - 博客园
目录
Monitor
Monitor 被称为监视器或者管程
这个概念来自于底层操作系统,它是如何与Java联系起来的呢?Java对象保存在内存中时,由以对象头,实例数据,对齐填充字节组成。
对象头又由 Mark Word,指向类的指针,数组长度(只有数组对象才有)组成。
每个Java对象都可以关联一个Monitor对象,当使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针
Monitor的结构如下
如何理解它们的关系呢?
synchronized 同步代码块的语法是 synchronized(obj) { // 临界区代码块 } ,obj是指定的类或者对象。
假设一个Thread2 想要执行synchronized里面的代码,就会尝试将obj这个对象里的Mark Word跟操作系统里的Monitor相关联,它依靠着指针地址指向Monitor,也就是说,obj对象里的Mark Word记录了指向Monitor的指针地址。
正常情况下(下图中 Normal的情况),Mark Word 记录 哈希码(hashcode),分代年龄(age)等信息,并且其标志位的数字是01(表示未和任何Monitor相关联)
Mark Word 的结构 ↓
如果其他线程想要进入同步代码块,例如Thread1和Thread3,他们会看obj锁是否关联一个monitor,再看owner上是不是有其他线程了,当前Thread2是monitor的主人,因此它们会和monitor的EntryList相关联(也就是一个等待队列/阻塞队列),直到Thread2执行完同步代码块里的代码,释放了锁,Thread1和Thread3才会被唤醒,互相竞争拿到monitor的owner的所有权
注意:
- synchronized必须是同一个对象的monitor才有上诉效果
- 不加synchronized的对象不会关联监视器,不遵从以上规则
Monitor工作原理字节码层面
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
通过JDK自带的javap命令查看上诉代码相关的字节码信息
从上面可以看出,synchronized同步语句块的实现使用的是monitorenter和monitorexit指令,其中monitorenter指令指向了同步代码块的开始位置,monitorexit指令则指向了同步代码块的结束位置。
当执行monitorenter指令时,线程视图获取锁,也就是monitor的持有权(onwner)
monitorenter 指令会将lock对象 Mark Word 置为指向monitor的指针
monitorexit 指令会将lock对象 Mark Word 重置,并唤醒EntryList
另外,如果程序出现异常,也将执行 monitorexit 指令,橘色方框框起来的位置就是异常时的处理,下面的 exception table 圈定了一个可能发生异常的范围 比如 4-14行,如果这段代码出现异常,就会到17行去执行,astore 指令加载lock引用,找到monitor,将lock对象里的Mark Word重置,并唤醒EntryList。然后抛出异常。
轻量级锁
当线程访问同步代码块的时候,会先判断锁的标志位
回顾一下 Mark Word 里每一种锁状态时,内容是什么:
当标志位为01的时候,无无锁状态
当标志位为00的时候,是轻量级锁。
如果是01,则说明是无锁状态,此时会创建锁记录(Lock Record),每个线程的栈帧都包含一个锁记录的结构,内部可以存储锁定对象的Mark Word。
在无锁状态下,让锁记录中的Object Reference指向锁对象,并尝试用CAS替换 Object的Mark Word,将Mark Word的值存入锁记录。
如果 CAS替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁。线程栈帧中则存储了原本属于object 对象里的 hashcode,age,bias 01 等信息(它们做了交换)
成功的标志就是 object的标志位有没有被替换掉,比如从01 (无锁)替换成了00(轻量级锁)
如果CAS失败,有两种情况:
- 如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀的过程
- 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record 作为重入计数
当发生synchronized锁重入的时候,Thread 的栈帧会再添加一条Lock Record 的记录,Object Reference 仍然指向了Object(同一个对象),只是它去CAS比对 对象头的时候会失败,因为Object 对象头里的Mark Word 已经被自己的上一个栈帧的Lock Record 更改了,不过Thread能从Object 的 lock record 里知道里面存放的的是自己上个栈帧里的lock record,这就是synchronized的锁重入,重入后创建的栈帧 Lock Record 里 存放了一个null值,这其实是锁重入的技术,加了几次锁,就看Lock Record的数量。
- 当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录 表示重入计数减一
直到退出synchronized代码块(解锁时),发现Lock Record 的锁记录的值不为null,这时使用CAS将Mark Word 的值恢复给对象头
- 成功,则解锁成功
- 失败,则说明轻量级锁进行了锁膨胀已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加锁了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
当Thread0已经对Object加上了轻量级锁的时候, 也就是将Object的Mark Word 已经替换成Thread0的Lock Record 记录的时候,Thread1想进入同步代码块,也想对Object加轻量级锁,但是Thread0已经对Object加上了轻量级锁,Thread1 想把自己的Lock Record记录与Object Mark Word 交换,但是失败了,因为Object Mark Word 里已经是Thread0的Lock Record地址了(标志位00,表明已经是轻量级锁了)
- 这时Thread1加轻量级锁失败,进入锁膨胀流程
- 即为Object 对象 申请Monitor锁,让Object 指向重量级锁地址
- 然后自己进入Monitor的EntryList,变为BLOCKED状态
锁对象Object里 Mark Word 的Thread0的Lock Record地址,就变成了申请的Monitor对象的地址了,当Thread0退出同步代码块时,使用CAS将Mark Word的值恢复给对象头,就会失败。这时就进入重量级锁的解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为NULL,唤醒EntryList中Blocked的线程
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(这个时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,很智能。
自旋占用CPU时间,单核CPU自旋会很浪费,多核CPU自旋才能发挥优势
Java 7 之后不能控制是否开启自旋功能了
偏向锁
轻量级锁在没有竞争的时候,每次重入仍然需要执行CAS操作。
Java6 中引入了偏向锁来做进一步的优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后就发现正线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有。
偏向状态
再回忆一下对象头格式:
Mark Word 记录了用于存储对象自身运行时的数据,如HashCode,GC分代年龄,锁状态标志,线程持有的锁,偏向锁线程ID等。
下面红框标识出来的地方 biased_lock,就是偏向锁的状态,正常情况下是 0 的标志位,当成为偏向锁的状态时变成了1
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么创建对象后,Mark Word 值 为0X05 即最后3位为101,这时thread,epoch,age 都为0
- 偏向锁默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数:-XX:BiasedLockingStartupDelay = 0 来禁用延迟
- 如果没有开启偏向锁,那么对象创建后,Mark Word 值为 0x01 即最后3位为001,这时它的hashcode,age都会0,第一次用到hashCode 时才赋值
上诉结论非常抽象,可以用jol-core依赖来用于验证,它会打印出Mark Word里的信息,来用于对象创建时是否符合上诉结论 ,主要就是看Mark Word 最后3位的值是多少,是否是偏向锁状态(本篇学习笔记不再演示代码用于验证)
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10-TEST</version>
</dependency>
撤销-调用对象的hashCode
调用了对象的hashCode,但偏向锁的对象Mark Word 中存储的是线程id,如果调用hashCode会导致偏向锁被撤销。
- 轻量级锁会记录锁记录中记录hashCode
- 重量级锁会在Monitor 中记录hashCode
在调用hashCode后使用偏向锁,记得去掉 -xx:useBiasedLocking
撤销-其他线程使用对象
当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID
当撤销偏向锁阈值超过20次以后,JVM会在给这些对象加锁时重新偏向至加锁线程(一种优化)
批量撤销
当撤销偏向锁阈值超过40次之后,JVM会这样觉得自己可能确实偏向错了,不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。