基础知识

线程切换代价

Java的线程是映射到操作系统的原生线程之上的,如果阻塞或唤醒一个线程就需要操作系统介入,需要在用户态和内核态之间切换,该切换会消耗大量的系统资源,因为用户态和内核态均有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递很多变量、参数给内核,内核也需要保护好用户态切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

JVM1.6之前,Synchronized会导致争不到锁的线程直接进入阻塞状态,所以说其是一个重量级的同步操作,被称为重量锁。

为了缓解上述的性能问题,JVM1.6开始,引入了偏向锁、轻量锁,其均属于乐观锁。

Mark Word

在JVM 1.6中,对象实例在堆内存中被分为3部分: 对象头、实例数据、对齐填充。

对象头的组成部分: Mark Word、指向类的指针、数组长度(可选,数组类型时才有),每个部分长度均为1个字宽,32位的JVM中,1字宽位32 bit,64位的JVM中,1字宽为64 bit。

锁升级功能主要依赖Mark Word中锁标志位和是否偏向锁标志位。

Synchronized同步锁的升级优化路径: 偏向锁->轻量级锁->重量级锁。

锁升级

偏向锁

偏向锁主要用来优化同一线程多次申请同一个锁的竞争,在某些情况下,大部分时间都是同一个线程竞争锁资源。

当线程1再次获取锁时,会比较当前线程ID与锁对象头Mark Word中的线程ID是否一致。

  • 如果一致,直接获取锁,无需CAS来抢占锁;
  • 如果不一致,需要查看锁对象头Mark Word中的线程是否存活:
    • 若存活,查找线程1的栈帧信息,如果线程1还需要继续持有该锁对象,那么暂停线程1(Stop-The-World),撤销偏向锁,升级为轻量级锁;如果线程1不再使用锁对象,则将锁对象设置为无锁状态(也属于锁撤销),然后重新偏向线程2;
    • 若不存活,则将锁对象设置为无锁状态(也属于锁撤销),然后重新偏向线程2。

可以看到,当持有锁的线程宕掉之后,其他请求锁的线程会检查持有锁的线程是否存活,若不存活则直接撤销锁,从而避免了死锁

在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁会被撤销,发生STW,加大性能开销。

JVM的默认配置为: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4000,即默认开启偏向锁,并且延迟4秒生效,之所以延迟,是因为JVM刚启动时竞争比较激烈。

关闭偏向锁: -XX:-UseBiasedLocking,也可以直接设置为重量级锁: -XX:+UseHeavyMonitors。

轻量锁

轻量锁适应的场景是: 各线程交替执行同步块,大部分的锁在同步周期内不存在长时间的竞争。

轻量锁在虚拟机内部是通过BasicObjectLock对象实现的,该对象内部由一个BasicLock对象_lock和一个锁对象指针_obj组成,BasicObjectLock对象放置在Java栈帧中。

在BasicLock内部还维护着displace_header字段,用于备份锁对象头部的Mark Word。

// A BasicObjectLock associates a specific Java object with a BasicLock.
// It is currently embedded in an interpreter frame.
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {private:BasicLock _lock;                        // the lock, must be double word alignedoop       _obj;                         // object holds the lock;
};
class BasicLock VALUE_OBJ_CLASS_SPEC {private:volatile markOop _displaced_header;
};

当需要判断一个线程是否持有该锁对象时,只需要简单判断锁对象头的指针是否在当前线程栈地址范围即可。

加锁

线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,即BasicObjectLock对象。

创建过程如下:

  • 将锁对象头的Mark Word拷贝赋值给BasicObjectLock中BasicLock对象的_displaced_header字段;
  • 然后线程尝试使用CAS将锁对象头的Mark Word替换为指向该BasicObjectLock对象的指针;
  • 若成功,则当前线程获得锁。若失败,表示其他线程竞争锁,当前线程尝试使用自旋来获取锁。

可以看到,当前线程即时获取锁失败,也不会立即阻塞挂起,而是先尝试使用自旋来获取锁。如果竞争不是很激烈,可能几次自旋,当前线程就获取到锁了,从而避免了1次线程的上下文切换。

若当前线程自旋获取锁失败,锁就会膨胀成重量锁,当前线程阻塞挂起。

解锁

轻量锁解锁时,会使用原子的CAS操作将BasicObjectLock对象备份的_displaced_header替换回到锁对象头的Mark Word。若成功,则表明没有竞争发生,解锁成功;若失败,则表明当前锁存在竞争(此时锁已经膨胀为重量锁),释放锁并唤醒阻塞的线程。

自旋锁

当锁处于轻量锁状态,且被某线程持有时,其他线程尝试获取锁失败后,不会直接阻塞挂起,而是先自旋一定次数,避免正在持有锁的线程可能在很短的时间内释放锁资源。

从JVM 1.7开始,自旋锁默认启用,自旋次数不宜设置过大(避免长时间占用CPU),-XX:+UseSpinning -XX:PreBlockSpin=10,JVM 1.7之后,默认的自旋次数由JVM根据实际系统环境灵活设置。

在锁竞争不是很激烈且锁占用的时间非常短的场景下,自旋锁可以通过减少上下文切换来提高系统性能;在锁竞争激烈或者锁占用时间较长的场景下,自旋锁会导致大量的线程一直处于CAS重试状态,造成CPU空转。

在高并发场景下,可以通过关闭自旋锁来优化系统性能: -XX:-UseSpinning。

该线程自旋之后仍旧未获取锁,则其会将锁对象升级为重量锁。未抢到锁的线程都会进入Monitor,之后会被阻塞到WaitSet中。

这里有个问题,假设持有轻量锁的线程执行同步块的时候宕掉了,则不会有释放锁并唤醒阻塞线程的动作,此时会造成死锁吗?

答案是肯定不会的,因为其他线程请求锁时,会有1个线程自旋获取锁失败后将锁升级为重量锁,然后获取重量锁的线程在释放的时候会将WaitSet中阻塞的线程唤醒,也就是说即使持有轻量级锁的线程不唤醒阻塞线程,其他持有重量级锁的线程在释放锁的时候也会唤醒WaitSet中的阻塞线程的,可谓是双重保障,哈哈

重量锁

当多个线程同时请求某个Monitor时,对象的Monitor会设置以下状态用来区分请求的线程:

  • Contention List: 所有请求锁的线程被首先放置到该竞争队列;
  • Entry List: Contention List中那些有资格成为候选人的线程会被转移到Entry List;
  • Wait Set: 调用Wait方法等被阻塞的线程会被放置到Wait Set;
  • OnDeck: 任何时刻仅能有1个线程竞争锁,该线程称为OnDeck;
  • Owner: 获取锁的线程称为Owner;
  • !Owner: 释放锁的线程。

EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发访问,为了降低对ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在 Hotspot中把OnDeck的选择行为称之为“竞争切换”。

OnDeck线程获得锁后即变为Owner线程,无法获得锁则会依然留在EntryList中,考虑到公平性,在EntryList中的位置不发生变化(依然在队头)。如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒,则再次转移到EntryList。

需要注意的是:当持有重量锁的线程在运行期间出错,会自动释放掉锁,从而避免死锁。

小结

锁升级的过程可以通过下面2张图来展示:


参考

  • https://www.cnblogs.com/lykm02/p/4516777.html
  • https://www.cnblogs.com/sevencutekk/archive/2019/09/21/11563367.html

Java的锁机制--偏向锁、轻量锁、自旋锁、重量锁相关推荐

  1. 2021面试 Lock,synch,dcl双检查锁sy+volite,悲观锁,偏向,轻量锁,重量锁,升级12

    0.数据库悲观锁:for update: MySQL实现悲观锁_九色鹿-CSDN博客_mysql悲观锁怎么实现 1. ReentrantLock锁公平与非公平实现.重入原理:  ReentrantLo ...

  2. mysql锁机制为何设计如此复杂_再谈mysql锁机制及原理—锁的诠释

    加锁是实现数据库并发控制的一个非常重要的技术.当事务在对某个数据对象进行操作前,先向系统发出请求,对其加锁.加锁后事务就对该数据对象有了一定的控制,在该事务释放锁之前,其他的事务不能对此数据对象进行更 ...

  3. 锁机制有什么用?简述Hibernate的悲观锁和乐观锁机制

    有些业务逻辑在执行过程中要求对数据进行排他性的访问,于是需要通过一些机制保证在此过程中数据被锁住不会被外界修改,这就是所谓的锁机制. Hibernate支持悲观锁和乐观锁两种锁机制.悲观锁,顾名思义悲 ...

  4. python锁机制_Python并发编程之谈谈线程中的“锁机制”(三)

    大家好,并发编程 进入第三篇. 今天我们来讲讲,线程里的锁机制. 本文目录 何为Lock( 锁 )?如何使用Lock( 锁 )?为何要使用锁?可重入锁(RLock)防止死锁的加锁机制饱受争议的GIL( ...

  5. 轻松掌握mysql数据库锁机制的相关原理_轻松掌握MySQL数据库锁机制的相关原理...

    不同于行级或页级锁定的选项: · 版本(例如,为并行的插入在MySQL中使用的技术),其中可以一个写操作,同时有许多读取操作.这明数据库或表支持数据依赖的不同视图,取决于访问何时开始.其它共同的术语是 ...

  6. java后台处理APP表情-使用轻量工具emoji-java处理emoji表情字符

    目录 pom依赖 java工具类 测试 Java Url编码转换 在APP开发中,大多需要涉及表情符号丰富APP,但是因为我们的数据库一般是utf8编码,是3个字节,而表情符号基本都是四个字节的uni ...

  7. 偏向锁、轻量锁、重量锁的理解

    java中每个对象都可作为锁,锁有四种级别,按照量级从轻到重分为:无锁.偏向锁.轻量级锁.重量级锁.并且锁只能升级不能降级. 在讲这三个锁之前,我先给大家讲清楚自旋和对象头的概念. 自旋 现在假设有这 ...

  8. 并发系列三:证明分代年龄、无锁、偏向锁、轻量锁、重(chong)偏向、重(chong)轻量、重量锁

    前言 上篇文章咱们了解了synchronized关键字的常见用法.对象头以及证明了一个对象在无锁状态下的对象头markwork部分的前56位存储的是hashcode.接下来,咱们继续来根据对象头分别证 ...

  9. Java 并发编程解析 | 如何正确理解Java领域中的锁机制,我们一般需要掌握哪些理论知识?

    苍穹之边,浩瀚之挚,眰恦之美: 悟心悟性,善始善终,惟善惟道! -- 朝槿<朝槿兮年说> 写在开头 提起Java领域中的锁,是否有种"道不尽红尘奢恋,诉不完人间恩怨"的 ...

最新文章

  1. git 服务器搭建,在自己服务器上搭建私有仓库
  2. C++中delete和delete[]的区别
  3. 【渝粤题库】陕西师范大学201661英语阅读(二)作业(高起专)
  4. 2021-06-27变量的作用域
  5. mysql获取变量_获取Mysql的状态、变量
  6. 应用架构、业务架构、技术架构和业务流程图详解
  7. 如何玩转抖音吸粉引流,老路子新热点照样1000+
  8. 检查一个字符串是否为回文 。 回文:正着念与反着念一样,例如:上海自来水来自海上
  9. 工行网银B2c第三方接口开发
  10. 关闭NV显卡的优化功能
  11. 腾讯视频怎么下载_QQ视频如何下载到本地保存
  12. javaweb项目运转流程
  13. [大数据]数据可视化 -- 练习卷(下)
  14. Linphone SDK 最新版移植 iOS版
  15. Java基础学习系列--(二)【抽象类,接口、代码块、final、单例、枚举】
  16. 解决 oracle 错误ORA-01033
  17. 分位数Granger因果检验实现原理
  18. CentOS opera 浏览器
  19. Android win10 平板 省电,Win10平板模式省电吗?如何设置?
  20. 如何嗅探并下载ts并合成视频文件,m3u8文件处理

热门文章

  1. python基础一:编码和解码
  2. 「面试必背」Tomcat面试题(收藏)
  3. image.save存储图像时的报错:cannot write mode F as JPEG
  4. 浅谈STM32在应用中编程(IAP)的应用(俗称在线更新程序)
  5. Cadence Virtuoso 电路元器件旁边的参数显示出来
  6. 阿里云、腾讯云、百度云的测评哪一个更好用
  7. easytrader交易接口推介:如何使用股票行情数据接口打板策略快人一步?
  8. java编写类骑士游历_骑士游历问题
  9. 『晨读』《孙子兵法》云:“故用兵之法,十则围之,五则攻之,倍则分之,敌则能战之,少则能逃之,不若则能避之。
  10. 开发调试时,清空浏览器缓存