synchronized 底层原理

学习来源:

黑马程序员全面深入学习Java并发编程,JUC并发编程全套教程_哔哩哔哩_bilibili

另外参考:

Java6及以上版本对synchronized的优化 - 蜗牛大师 - 博客园

目录

Monitor

Monitor工作原理字节码层面

轻量级锁

锁膨胀

自旋优化

偏向锁

偏向状态

撤销-调用对象的hashCode

撤销-其他线程使用对象

批量重偏向

批量撤销


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同步语句块的实现使用的是monitorentermonitorexit指令,其中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会这样觉得自己可能确实偏向错了,不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值