更过博文请关注:https://blog.bigcoder.cn

JDK 1.6后锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁,这四种锁状态分别代表什么,为什么会有锁升级?

其实在 JDK 1.6之前,synchronized 还是一个重量级锁,底层使用操作系统的 Mutex Lock(互斥锁)实现,而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么重量级锁效率低的原因。

但是在JDK 1.6后,JVM为了提高锁的获取与释放效率对(synchronized )进行了优化,引入了 偏向锁 和 轻量级锁 ,从此以后锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,也就是说只能进行锁升级(从低级别到高级别),不能锁降级(高级别到低级别),这种设计目的是为了提高获得锁和释放锁的效率。

一. 对象的内存布局

要弄清楚加锁过程到底发生了什么需要看一下对象创建之后再内存中的布局是个什么样的?

一个对象在new出来之后在内存中主要分为4个部分:

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

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

  • instance data:记录了对象里面的变量数据。

  • padding:作为对齐使用,对象在64位服务器版本中,规定对象内存必须要能被8字节整除,如果不能整除,那么就靠对齐来补。举个例子:new出了一个对象,内存只占用18字节,但是规定要能被8整除,所以padding=6。

知道了这4个部分之后,我们来验证一下底层。借助于第三方包 JOL( Java Object Layout)内存布局去看看。首先我们先引入JOL依赖:

<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>

编写以下代码,查看Object内存布局:

public class JOLDemo {
private static Object o;
public static void main(String[] args) {
o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}

synchronized不论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步,那么Synchronized锁对象是存在哪里的呢?答案是存在锁对象的对象头的markword中。那么markword在对象头中到底长什么样?

前面我们说过,markword中的数据结构会在运行时期随着锁标识位的变化而发生变化的,而markword分为下列几种状态:

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

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

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

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

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

二. 锁的分类

2.1 无锁

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

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

2.2 偏向锁

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。另外,JVM对那种会有多线程加锁,但不存在锁竞争的情况也做了优化,听起来比较拗口,但在现实应用中确实是可能出现这种情况,因为线程之前除了互斥之外也可能发生同步关系,被同步的两个线程(一前一后)对共享对象锁的竞争很可能是没有冲突的。对这种情况,JVM用一个epoch表示一个偏向锁的时间戳(真实地生成一个时间戳代价还是蛮大的,因此这里应当理解为一种类似时间戳的identifier)

2.3.1 偏向锁的获取

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

2.3.2 偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word,要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

2.4 轻量级锁(CAS自旋实现)

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。

轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示同步过程已完成。如果失败,表示有其他线程尝试过获取该锁,则要在释放锁的同时唤醒被挂起的线程。

2.5 重量级锁

重量级锁对应的锁标志位是10,存储了指向重量级监视器锁的指针,在HotSpot中,对象的监视器(monitor)锁对象由ObjectMonitor对象实现(C++),其跟同步相关的数据结构如下:

ObjectMonitor() {
_count = 0; //用来记录该对象被线程获取锁的次数
_waiters = 0;
_recursions = 0; //锁的重入次数
_owner = NULL; //指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
}

光看这些数据结构对监视器锁的工作机制还是一头雾水,那么我们首先看一下线程在获取锁的几个状态的转换:

线程的生命周期存在5个状态,start、running、waiting、blocking和dead

对于一个synchronized修饰的方法(代码块)来说:

  1. 当多个线程同时访问该方法,那么这些线程会先被放进_EntryList队列,此时线程处于blocking状态
  2. 当一个线程获取到了实例对象的监视器(monitor)锁,那么就可以进入running状态,执行方法,此时,ObjectMonitor对象的_owner指向当前线程,_count加1表示当前对象锁被一个线程获取。
  3. 当running状态的线程调用wait()方法,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1,同时线程进入_WaitSet队列,直到有线程调用notify()方法唤醒该线程,则该线程会进入_EntryList队列中

三. 锁的升级路线

3.1 升级为偏向锁

从锁升级的图中能看到两个路线,“偏向锁未启动–>普通对象”和“偏向锁已启动–>匿名偏向对象”什么意思呢?原来我们是可以配置偏向锁未启动和偏向锁已启动的。 操作系统有个参数-XX:BiasedLockingStartupDelay=0从字面意思,偏向锁的启动延迟。JVM默认情况下会在启动后4s开启偏向锁。

我们编写下列代码来验证“无锁->偏向锁”的转化过程:

public class JOLDemo {
private static Object o;
public static void main(String[] args) {
o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}

默认情况下偏向锁会在程序启动后4s后才会开启(如有必要可以使用JVM参数来关闭延迟-XX:BiasedLockingStartupDelay = 0)所以的上述代码致使之中都没有开启偏向锁,所以最终效果就是“无锁->轻量级锁”。

前文我们说过markword由64位(8个字节)组成,现实生活中我们通常把高位数字放在前面,在计算机中通常会把低位字节放在前面,这也就是为什么打印出来的内存布局中第一行的第一个字节最后两个bit是锁标志位,而不是第二行最后一个字节最后两个bit是锁标志位的原因。

我们可以通过Thread.sleep(5000)来使得虚拟机开启偏向锁:

public class JOLDemo {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}

3.2 升级为重量级锁

这是一个相当复杂的过程,在JDK1.6之前,有两种情况,某个线程的自旋次数超过10次(JVM调优可调),等待的自旋的线程数(JVM调优)超过了CPU核数的二分之一。满足这两个条件的任意一个,操作系统会把这些线程丢到等待队列,膨胀为重量级锁。在JDK1.6之后,出现了自适应自旋。什么意思呢?JDK根据运行的情况和每个线程运行的情况决定要不要升级。

public class JOLDemo {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable()); new Thread(() -> {
synchronized (o) {
//由于是第一个线程获取锁,此处应该是偏向锁
System.out.println(ClassLayout.parseInstance(o).toPrintable());
try {
//sleep 5s不释放锁
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(1000);
new Thread(() -> {
synchronized (o) {
//由于上一个线程长时间持有锁,CAS超过阈值后膨胀为重量级锁
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}).start();
}
}

本文参考至:

Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级_tongdanping的博客-CSDN博客

谈谈JVM内部锁升级过程 (qq.com)

深度分析:锁升级过程和锁状态,看完这篇你就懂了! - SegmentFault 思否

Synchronized你以为你真的懂? - 知乎 (zhihu.com)

synchronized锁升级过程的更多相关文章

  1. 详细了解 synchronized 锁升级过程

    前言 首先,synchronized 是什么?我们需要明确的给个定义--同步锁,没错,它就是把锁. 可以用来干嘛?锁,当然当然是用于线程间的同步,以及保护临界区内的资源.我们知道,锁是个非常笼统的概念 ...

  2. 并发编程:synchronized 锁升级过程的验证

        关于synchronized关键字以及偏向锁.轻量级锁.重量级锁的介绍广大网友已经给出了太多文章和例子,这里就不再重复了,也可点击链接来回顾一下.在这里来实战操作一把,验证JVM是怎么一步一步 ...

  3. 关于Synchronized的偏向锁,轻量级锁,重量级锁,锁升级过程,自旋优化,你该了解这些

    前言 相信大部分开发人员,或多或少都看过或写过并发编程的代码.并发关键字除了Synchronized(如有不懂请移至传送门,关于Synchronized的偏向锁,轻量级锁,重量级锁,锁升级过程,自旋优 ...

  4. Synchronized锁升级原理与过程深入剖析

    Synchronized锁升级原理与过程深入剖析 前言 在上篇文章深入学习Synchronized各种使用方法当中我们仔细介绍了在各种情况下该如何使用synchronized关键字.因为在我们写的程序 ...

  5. synchronized锁升级详细过程

    java对象头由3部分组成: 1.Mark Word 2.指向类对象(对象的class对象)的指针 3.数组长度(数组类型才有) 重点是 Mark Word结构,下面以32位HotSpot为例: 一. ...

  6. 再谈synchronized锁升级

    在图文详解Java对象内存布局这篇文章中,在研究对象头时我们了解了synchronized锁升级的过程,由于篇幅有限,对锁升级的过程介绍的比较简略,本文在上一篇的基础上,来详细研究一下锁升级的过程以及 ...

  7. 简单的理解synchronized锁升级

    前言 今天碰到一个synchronized锁升级的问题, 查了查, 发现一个帖子举例说明比较贴切, 特此转发, 如有问题, 欢迎讨论说明 转自: 木叶盒子 https://www.bilibili.c ...

  8. Synchronized锁升级

    Synchronized锁升级 锁的4中状态:无锁状态.偏向锁状态.轻量级锁状态.重量级锁状态(级别从低到高) 为什么要引入偏向锁? 因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞 ...

  9. 深入并发锁,解析Synchronized锁升级

    这篇文章分为六个部分,不同特性的锁分类,并发锁的不同设计,Synchronized中的锁升级,ReentrantLock和ReadWriteLock的应用,帮助你梳理 Java 并发锁及相关的操作. ...

  10. JAVA对象分析之偏向锁、轻量级锁、重量级锁升级过程

    在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分: 对象头(Header) 实例数据(Instance Data) 对齐填充(Padding). 对象头 HotSpot虚拟机(后面 ...

随机推荐

  1. HarmonyOS:使用MindSpore Lite引擎进行模型推理

      场景介绍 MindSpore Lite是一款AI引擎,它提供了面向不同硬件设备AI模型推理的功能,目前已经在图像分类.目标识别.人脸识别.文字识别等应用中广泛使用. 本文介绍使用MindSpore ...

  2. nginx重新整理——————http请求的11个阶段中的content阶段[十八]

    前言 简单介绍一下content 阶段. 正文 下面介绍一下root和alias. 这个前面其实就提交过了,这里再说明一下. 功能都是一样的:将url映射为文件路径,以返回静态文件内容. 差别:roo ...

  3. python实现:有一个列表为num_list,找到一个具有最大和的连续子列表,返回其最大和。

    # 有一个列表为num_list,找到一个具有最大和的连续子列表,返回其最大和.# 示例:# 输入: [-3,1,-1,6,-1,2,4,-5,4]# 输出: 11# 解释: 连续子数组 [6,-1, ...

  4. 国产GOWIN实现低成本实现CSI MIPI转换DVP

    CSI MIPI转换DVP,要么就是通用IC操作,如龙讯芯片和索尼芯片,但是复杂的寄存器控制器实在开发太累.对于FPGA操作,大部分都是用xilinx的方案,xilinx方案成本太高,IP复杂. 而用 ...

  5. 海康摄像机&大华摄像机&DSS平台的RTSP流地址格式

    实时流 海康: rtsp://[username]:[password]@[ip]:[port]/[codec]/[channel]/[subtype]/av_stream 说明:username: ...

  6. SQL case when then end

    SQL case when then end sql中的返回值可以使用case when then end来进行实现 SELECT s.s_id, CASE s.s_name WHEN '1' THE ...

  7. 力扣569(MySQL)-员工薪水中位数(困难)

    题目: 写一个SQL查询,找出每个公司的工资中位数,以任意顺序返回结果表.查询结果个数如下所示. 输出结果如下:  解题思路: 中位数:位于集合正中间的元素.当数据总书为奇数时,最中间的数就是中位数, ...

  8. 力扣224(java)-基本计算器(困难)

    题目: 给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值. 注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval() . 示例 1: 输入:s = " ...

  9. 谈AK管理之基础篇 - 如何进行访问密钥的全生命周期管理?

    简介: 我们也常有听说例如AK被外部攻击者恶意获取,或者员工无心从github泄露的案例,最终导致安全事故或生产事故的发生.AK的应用场景极为广泛,因此做好AK的管理和治理就尤为重要了.本文将通过两种 ...

  10. SAE助力「海底小纵队学英语」全面拥抱Serverless,节省25%以上成本

    简介: 阿里云Serveless应用引擎SAE 具备免运维IaaS.按需使用.按量计费.低门槛服务应用上云,并且支持多种语言和高弹性能力等特点,刚好完美解决了客户长期以来运维复杂.资源利用率不高.开发 ...