写点什么

【Java 并发实战】偏向锁 - 轻量级锁 - 重量级锁,掌握这些知识点再也不怕面试通不过

  • 2021 年 11 月 10 日
  • 本文字数:2166 字

    阅读完需:约 7 分钟

}


}


public void sync4() {


synchronized(MyTest.css) {


// do somethings


}


}


同步方法块是需要根据方法中具体同步的对象来实现的。


在上面代码中其实 sync3()跟同步普通方法一样,锁的是当前实例对象;那么 sync4 方法就与同步静态方法一样,锁的是当前类的 class 对象。


从上面代码可以看出来的,我们通过使用 synchronized 关键字可以很简单的解决并发问题,但是其实是 jvm 底层通过使用一种叫内置锁的手段,简化了开发人员实现并发的复杂度,在 jdk1.6 以前 synchronized 是基于重量锁实现的,即每次遇到同步代码都要获取锁,然后释放锁,在 jdk1.6 之后对其优化,根据不同场景使用不同的策略,这也就是 偏向锁、轻量锁、重量锁的来由。在介绍他们之前我先介绍一下另一个锁-自旋锁。听到这么多锁,是不是头晕,当初我学习的时候也是这样的。但是当你慢慢学习深入,你就会很容易的理解每个锁的作用


自旋锁




自旋锁顾明思意就是旋转等待的意思,那么它的作用是什么呢?


1.当前线程尝试去竞争锁


2.竞争失败,准备阻塞自己


3.但是并没有阻塞自己,而是采用自旋锁,进入自旋状态。


4.进入自旋状态,并且重新不断竞争锁。


5.如果自旋期间成功获取锁,那么结束自旋状态,否则进入阻塞状态。


如果在自旋期间成功获取锁,那么就减少一次线程的切换。


根据上面的解释我们可以很容易理解自旋锁的意义,因为 CPU 从内核状态切换至用户状态,线程的阻塞与恢复都会浪费资源的,但是通过自旋而不是去阻塞当前线程,那么就会节省这一个 CPU 状态的切换。


所以自旋锁适合在持有锁时间长,并且竞争不激烈的场景下使用。


使用-XX:-UseSpinning 参数关闭自旋锁优化;-XX:preBlockSpin 参数修改默认的自旋次数。


偏向锁




在实际场景中,如果一个同步方法,没有多线程竞争,并且总是由同一个线程多次获取锁,在这种场景下,如果每次还有阻塞线程,唤醒 cpu 从用户状态转核心态,那么对于 cpu 是一种资源的浪费,为了解决这类问题,就引入了偏向锁的概念。


“偏向”的意思是,偏向锁假设将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在 Mark word 中 CAS 记录 owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁的状态为偏向锁;否则说明有其他线程竞争,膨胀为轻量级锁。


具体的步骤如下:


1.访问同步代码块


2.检查对象头是都 owner 是否存储当前线程的 id


3.如果没有,进行 CAS 尝试替换 mark word 中的 owner。如果有执行同步代码块(代表获取锁成功)


4.修改成功(代表无竞争)owner 修改为当前线程的 id,执行同步代码块。修改失败(代表有竞争)进入撤销偏向锁,暂停线程并将 owner 置空,进入轻量级锁。


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码



偏向锁无法使用自旋锁,因为一旦有其他线程申请资源,就破坏了偏向锁的假定。


如果你确定应用程序中所有的锁通常是在竞争状态,你可以通过 JVM 参数关闭偏向锁


UseBiasedLocking = false,那么程序会默认进入轻量锁状态。


轻量锁




偏向锁是为了解决同步代码在单线程下访问性能问题。


轻量级锁是为了解决减少无实际竞争情况下,使用重量级锁产生的性能消耗


轻量锁,顾名思义,轻量是相对于重量的来说的,使用轻量级锁时,不需要申请互斥量(mutex),而是将 mark word 中的信息复制到当前线程的栈中,然后通过 CAS 尝试修改 mrak word 并替换成轻量锁,如果替换成功则执行同步代码块。如果此时有线程 2 来竞争,并且他也尝试 cas 修改 mark word 但是失败了,那么线程 2 进入自旋状态,如果在自旋状态也没有修改成功,那么轻量锁将膨胀成重量级锁,mark word 会被修改成重量锁标记(10,),线程进入阻塞状态。


当然,由于轻量级锁天然瞄准不存在锁竞争的场景,及时存在锁竞争但是也不激烈,仍然可以通过使用自旋锁优化,自旋失败之后再膨胀称为重量级锁。


重量锁




在 JVM 规范中,synchronized 是基于监视器锁(monitor)来实现的,它会在同步代码之前添加一个 monitorenter 指令,同时在同步代码结束处和异常处添加一个 monitorexit 指令去释放该对象的 monitor。需要注意的是每一个对象都有一个 monitor 与之配对,当一个 monitor 被获取之后,也就是被 monitorenter,它会处于锁定状态,其他尝试获取该对象的 monitor 线程会获取失败,只有当获取该对象的 monitor 的线程执行了 monitorexit 指令后,其他线程才有可能获取该对象的 monitor 成功。


所以从上面描述可以得出,监视器锁就是 monitor 它是互斥的(mutex)。由于它是互斥的,那么它的操作成本就非常的高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。


小结




偏向锁、轻量级锁、重量级锁适用于不同的并发场景:


偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。


轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。


重量级锁:有实际竞争,且锁竞争时间长。


另外,如果锁竞争时间短,可以使用自旋锁进一步优化轻量级锁、重量级锁的性能,减少线程切换。


如果锁竞争程度逐渐提高(缓慢),那么从偏向锁逐步膨胀到重量锁,能够提高系统的整体性能。


同时需要注意锁可以升级,但是不能降级。


另外通过这次学习,大家应该也知道自从 jdk1.6 以后 synchronized 已经被优化了,性能不会比 Lock 差


所以 jdk.16 版本及其以后版本的同学可以放心大胆的使用了。


最后附一张从偏向锁膨胀至重量锁的完全的流程图

评论

发布
暂无评论
【Java并发实战】偏向锁-轻量级锁-重量级锁,掌握这些知识点再也不怕面试通不过