【JUC】二十九、synchronized锁升级之轻量锁与重量锁

文章目录

  • 1、轻量锁
  • 2、轻量锁的作用
  • 3、轻量锁的加锁和释放
  • 4、轻量级锁的代码演示
  • 5、重量级锁
  • 6、重量级锁的原理
  • 7、锁升级和hashcode的关系
  • 8、锁升级和hashcode关系的代码证明
  • 9、synchronized锁升级的总结
  • 10、JIT编译器对锁的优化:锁消除和锁粗化
  • 11、结语

📕相关笔记:

【synchronized锁升级之 无锁】
【synchronized锁升级之 偏向锁】

1、轻量锁

前面一篇提到偏向锁,即只有一个线程在竞争,此时通过资源类对象的对象头的Mark Word来标记,避免了用户态和内核态的频繁切换。

在这里插入图片描述

再往下,又来了一个线程也来竞争这个锁,且此时这两个线程近乎可以错开交替执行(或者说同步代码块/方法执行一次时间很短,哪怕另一个线程等,也不会等太久),如下图的1、2、3、4标号:

在这里插入图片描述

这就是轻量级锁的出现场景:有线程来参与竞争了,但不存在锁竞争太过激烈的情况,获取锁的冲突时间极端,本质就是CAS自旋锁,不要直接往重锁走。对应的共享对象内存图:

在这里插入图片描述

2、轻量锁的作用

轻量锁是为了在两个线程近乎交替执行同步块时来提高性能。

直白说就是先CAS自旋,不行了再考虑升级为重锁,使用操作系统的互斥量。升级到轻量锁的时机有:

  • 关闭了偏向锁
  • 多线程竞争偏向锁,可能导致偏向锁升级为轻量锁(这里写可能,是因为如果恰好是一个线程over,一个线程上位,则依旧是偏向锁)

举个例子:比如现有A线程拿到了锁,A一个人走偏向锁玩了一会儿后,线程B来了,B在争抢时发现共享对象的对象头中Mark Word里的线程ID标记不是线程B的ID(而是线程A),此时,B线程通过CAS来尝试修改标记。当:

  • 此时线程A刚好Over,B上位,修改Mark Word里的线程ID为B,此时,仍为偏向锁,且偏向B

在这里插入图片描述

  • 如果A正在执行,B修改失败,则升级为轻量级锁,且轻量级锁继续由原来的线程A持有,接着执行刚才没执行完的,而线程B则自旋等待获取这个轻量级锁

在这里插入图片描述

3、轻量锁的加锁和释放

加锁:

JVM会在线程的栈帧中创建用于存储锁记录Lock Record的空间,称为Displaced Mark Word。

在这里插入图片描述

若一个线程获得锁时发现是轻量级锁,会把对象锁的MarkWord复制到自己的Displaced Mak Word里面。然后线程尝试用CAS将锁的MarkWord替换为指向锁记录的指针。如下面两幅草图示意的变化过程:

在这里插入图片描述

在这里插入图片描述

如果替换成功,当前线程获得轻量锁。如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁(自旋一定次数后仍未获得锁,升级为重量锁)。

轻量级锁的释放:

在释放锁时,当前线程会使里CAS操作将Displaced Mark Word的内容复制回对象锁的Mark Word里面。如果没有发生竞争。那么这个复制的操作会成功。如果持有锁期间有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。

4、轻量级锁的代码演示

-XX:-UseBiasedLocking

添加JVM参数,关闭偏向锁,就可以直接进入轻量级锁:

Object object = new Object();
new Thread(() -> {synchronized (object){System.out.println(ClassLayout.parseInstance(object).toPrintable());}
}).start();

运行:

在这里插入图片描述

轻量锁下,自旋达到一定次数或者说程度,会升级为重量锁:

  • Java6之前,默认情况下自旋的次数是10次或者自旋的线程数超过了cpu核数的一半,可-XX:PreBlockSpin=10来修改
  • Java6之后,JVM做了优化,采用自适应自旋

自适应自旋,即线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。反之,如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,以避免CPU空转。直白说就是会总结前人的经验了、会预判走位了。

轻量锁与偏向锁的区别:

  • 偏向锁是一个线程自己在玩,而偏向锁涉及竞争,且争夺轻量级锁失败时,自旋尝试抢占锁
  • 轻量级锁每次退出同步块都需要释放锁(要不就不会是一个走了一个接上了),而偏向锁则只在有线程来竞争时才释放锁

5、重量级锁

竞争太激烈时,只能捅到重量级锁,进行内核态和用户态的切换,但前面偏向锁和轻量级锁已然做了一定程度的缓冲和优化了。

在这里插入图片描述

有大量的线程参与锁的竞争,冲突性很高:

Object object = new Object();
//多个线程
for (int i = 0; i < 6; i++) {new Thread(() -> {synchronized (object){System.out.println(ClassLayout.parseInstance(object).toPrintable());}},String.valueOf(i)).start();
}

运行:

在这里插入图片描述

6、重量级锁的原理

Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。

当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor。

7、锁升级和hashcode的关系

在这里插入图片描述

可以看到,无锁状态下,Java对象头的Mark Word中是有空间存hashcode的,锁升级后,则没有位置了,那要是锁升级后hashcode去哪儿了 ?

在这里插入图片描述

总结下:

1) 在无锁状态下,Mark Word可以存储对象的identity hash code值,当对象的hashCode()方法第一次被调用时,JVM会生成对应的identity hash code值,存于对象头的Mark Word中。

2) 对于偏向锁,在线程获取偏向锁时,用Thread Id和epoch值(看成时间戳)去覆盖identity hash code所在的位置。如果一个对象的hashcode()方法已经被调用过一次,则这个对象不能被设置偏向锁,因为如果可以,那identity hash code就会被线程ID覆盖,就会造成同一对象,前后两次调用hashcode方法得到的结果不一致。

3) 升级为轻量锁时,JVM会在当前线程的栈帧中创建一个锁记录空间Lock Record(前面已提到),用于拷贝和存储锁对象的Mark Word,里面自然包含了identity hash code、GC年龄等,且释放轻量锁时,这些数据又会写回对象头,因此轻量级锁可以和identity hash code共存。

4) 到重量级锁时,Mark Word保存的是重量级锁指针,而代表重量级锁的ObiectMonitor类里有字段记录了非加锁状态下的Mark Word,锁释放以后也会写回对象头。

8、锁升级和hashcode关系的代码证明

Case1:当一个对象已经计算过identity hashcode,它就无法进入偏向锁状态,会跳过偏向锁,直接升级轻量级锁

//先睡5秒,抵消偏向锁开启的延时,保证开启偏向锁
TimeUnit.SECONDS.sleep(5);
Object object = new Object();
System.out.println("这里应该是偏向锁==>");
System.out.println(ClassLayout.parseInstance(object).toPrintable());
//没有重写hashcode,重写后无效
int hashCode = object.hashCode();
//验证当一个对象已经计算过identity hash code后,就无法进入偏向状态
new Thread(() -> {synchronized (object){System.out.println("这里本应是偏向锁,但刚才计算过一致性哈希hashcode,这里会直接升级为轻量级锁 ==>");System.out.println(ClassLayout.parseInstance(object).toPrintable());}
}).start();

在这里插入图片描述

Case2:偏向锁过程中遇到一致性哈希计算请求,立马撤销偏向模式,膨胀为重量级锁

//先睡5秒,抵消偏向锁开启的延时,保证开启偏向锁
TimeUnit.SECONDS.sleep(5);
Object object = new Object();
synchronized (object){System.out.println(ClassLayout.parseInstance(object).toPrintable());System.out.println("此时是偏向锁,但下面一计算哈希,会立马撤销偏向模式,膨胀为重量级锁");//计算哈希值,这里的hashcode方法是没有重写过的int hashCode = object.hashCode();System.out.println(ClassLayout.parseInstance(object).toPrintable());
}

在这里插入图片描述

9、synchronized锁升级的总结

在这里插入图片描述

synchronized锁升级,目的还是实现一个性能优化,思想就是:先自旋,不行了再阻塞。一直都是围绕尽量避免内核态和用户态频繁切换来展开的。实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式。太精辟了这句!道出了这几种锁的关系。

在这里插入图片描述

另外,synchronized在修饰方法和代码块时,在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。

请添加图片描述
最后 :

  • 偏向锁:适用于单线程的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。
  • 轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似),轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话(就很容易一个线程完事儿了,另一个线程尚未,哪怕不是这么刚刚好,也自旋等不了太久),采用轻量级锁自旋虽然会占用Cpu资源,但是相对比使用重量级锁还是更高效。
  • 重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁一更严重,这时候就需要升级为重量级锁。

10、JIT编译器对锁的优化:锁消除和锁粗化

JIT,即Just Time Compiler,翻译:即时编译器。

synchronized锁消除

以下是一个简单的synchronized代码,没啥毛病(别说优化成线程池):

public class LockClearDemo {static Object objectLock = new Object();public void m1(){synchronized (objectLock){System.out.println("----hello clearDemo");}}public static void main(String[] args) {LockClearDemo lockClearDemo = new LockClearDemo();for (int i = 0; i < 10; i++) {new Thread(() -> {lockClearDemo.m1();},String.valueOf(i)).start();}}
}

但此时,做出这样一个修改:

在这里插入图片描述

这么写,看似有synchronized,语法也没报错,实际每个线程进来都new一个自己的object对象,相当于是每一个线程一个自己创造的锁,而不是正常的所有线程共同抢一个对象的锁,因此,这么写毫无意义,JIT编译器会无视它,极端的说就是根本没有加这个锁对象的底层的机器码,是消除了锁的使用。

synchronized锁粗化

看示例代码:

public class LockDemo {static Object objectLock = new Object();public static void main(String[] args) {new Thread(() -> {synchronized (objectLock){System.out.println("111111");}synchronized (objectLock){System.out.println("222222");}synchronized (objectLock){System.out.println("333333");}synchronized (objectLock){System.out.println("444444");}}).start();}}

注意,这不是可重入锁,这里是频繁加锁解锁。虽然无语法错误,但底层编译器会把它合并优化为:

在这里插入图片描述

锁粗化:假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能。

11、结语

  • 没有锁:自由自在
  • 偏向锁:唯我独尊
  • 轻量锁:楚汉争霸
  • 重量锁:群雄逐鹿

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/222047.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基士得耶速印机印件故障解决方法和印刷机使用注意事项

基士得耶和理光两个品牌的一体化速印机同属于理光公司的两个不同品牌。基士得耶速印机的每个机型&#xff0c;都有和它通用的理光速印机的机型相对应。&#xff08;油墨版纸通用&#xff0c;外观一样&#xff0c;配件全部通用。&#xff09;速印机在印刷的时候&#xff0c;经常…

USB2.0 Spec 中文篇

体系简介 线缆 USB 是一种支持热拔插的高速串行传输总线&#xff0c;使用一对&#xff08;两根&#xff09;差分信号来传输数据&#xff0c;半双工。要求使用屏蔽双绞线。 供电 USB 支持 “总线供电” 和 “自供电” 两种供电模式。在总线供电方式下&#xff0c;设备最多可…

Tofu目标识别跟踪模块

Tofu3 是多波段视频物体识别跟踪模块&#xff0c;支持可见光视频与红外视频的输入&#xff0c;支持激光补光变焦自适应控制&#xff0c;支持视频下的多类型物体检测、识别、跟踪等功能。 产品支持视频编码、设备管理、目标检测、深度学习识别、跟踪等功能&#xff0c;提供多机…

智慧灯杆技术应用分析

智慧灯杆是指在传统灯杆的基础上&#xff0c;通过集成多种先进技术实现城市智能化管理的灯杆。智慧灯杆技术应用的分析如下&#xff1a; 照明功能&#xff1a;智慧灯杆可以实现智能调光、时段控制等功能&#xff0c;根据不同的需求自动调节照明亮度&#xff0c;提高照明效果&am…

利用Pytorch预训练模型进行图像分类

Use Pre-trained models for Image Classification. # This post is rectified on the base of https://learnopencv.com/pytorch-for-beginners-image-classification-using-pre-trained-models/# And we have re-orginaized the code script.预训练模型(Pre-trained models)…

大型科技公司与初创公司:选择哪一个?

你有没有想过&#xff0c;特别是在你职业生涯的开始&#xff0c;选择什么类型的公司&#xff1f;它应该是一家像谷歌、亚马逊、Meta 这样的大型科技公司&#xff0c;还是为一家小型初创公司工作。在本文中&#xff0c;我们将讨论实际差异是什么&#xff0c;并帮助你选择最适合你…

『OPEN3D』1.8.3 多份点云配准

多份点云配准是将多份点云数据在全局空间中对齐的过程。通常,输入是一组数据(例如点云或RGBD图像){Pi}。输出是一组刚性变换{Ti},使得经过变换的点云在全局空间中对齐。 NNNNNathan 本专栏地址: https://blog.csdn.net/qq_41366026/category_12186023.html 此处是…

DHCP—动态主机配置协议

动态主机配置协议DHCP&#xff08;Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议&#xff09;是RFC 1541&#xff08;已被RFC 2131取代&#xff09;定义的标准协议&#xff0c;该协议允许服务器向客户端动态分配IP地址和配置信息。 DHCP协议支持C/S&#x…

RocketMQ 总体概括

目录 概述RocketMQ 领域模型MQ 解决的问题电商平台案例初步设计引入中间件设计 MQ 选型结束 概述 官网地址 RocketMQ 领域模型 官方领域模型概述 下面图&#xff0c;是在自己理解的基础上&#xff0c;对官方的模型图添加了一些。 Topic&#xff1a;主题&#xff0c;可以理解…

Java网络编程——基于UDP的数据报和套接字

java.net.ServerSocket与java.net.Socket建立在TCP的基础上。TCP是网络传输层的一种可靠的数据传输协议。如果数据在传输途中被丢失或损坏&#xff0c;那么TCP会保证再次发送数据&#xff1b;如果数据到达接收方的顺序被打乱&#xff0c;那么TCP会在接收方重新恢复数据的正确顺…

扬声器(喇叭)

扬声器(喇叭) 电子元器件百科 文章目录 扬声器(喇叭)前言一、扬声器(喇叭)是什么二、扬声器(喇叭)的类别三、扬声器(喇叭)的应用场景四、扬声器(喇叭)的作用原理总结前言 扬声器广泛应用于音响系统、公共广播系统、汽车音响、电视、电脑和移动设备等各种电子设备…

Linux基本开发工具

编译器和自动化构建工具 一、编译器——gcc、g1. 安装 gcc/g2. 使用3. 链接库4. 拓展命令&#xff1a;od/file/ldd/readelf 二、自动化构建项目——make、makefile1. 介绍2. 使用例子touch——change file timestampsstat——display file or file system status修改时间 .PHON…

Qt 文字描边(基础篇)

项目中有时需要文字描边的功能 1.基础的绘制文字 使用drawtext处理 void MainWindow::paintEvent(QPaintEvent *event) {QPainter painter(this);painter.setRenderHint(QPainter::Antialiasing, true);painter.setRenderHint(QPainter::SmoothPixmapTransform, true);painte…

ceph的osd盘删除操作和iscsi扩展

ceph的osd盘删除操作 拓展:osd磁盘的删除(这里以删除node1上的osd.0磁盘为例) 1, 查看osd磁盘状态 [rootnode1 ceph]# ceph osd tree ID CLASS WEIGHT TYPE NAME STATUS REWEIGHT PRI-AFF -1 0.00298 root default -3 0.00099 host node10 hdd 0.00…

【Vins轨迹】pose_graph位姿图加载EVO精度评定

1. Vins的位姿图加载功能 如果想要对slam运行后的位姿轨迹进行评定&#xff0c;需要将数据保存到output文件夹中。 其中pose_graph.txt含有的信息&#xff1a;关键帧id、时间戳、vio的xyz、优化后的xyz、vio的四元数、优化后的四元数、回环到的关键帧id、回环信息&#xff08…

【十】python复合模式

10.1 复合模式简介 在前面的栏目中我们了解了各种设计模式。正如我们所看到的&#xff0c;设计模式可分为三大类:结构型、创建型和行为型设计模式。同时&#xff0c;我们还给出了每种类型的相应示例。然而&#xff0c;在软件实现中&#xff0c;模式并是不孤立地工作的。对于所…

HPM5300系列--第一篇 命令行开发调试环境搭建

一、目的 在之前的博客中《HPM6750系列--第二篇 搭建Ubuntu开发环境》、 《HPM6750系列--第三篇 搭建MACOS编译和调试环境》我们介绍了HPM6750evkmini开发环境的搭建过程&#xff0c;由于HPM5300系列共用一套hpm-sdk&#xff0c;故HPM5300的开发调试环境的搭建过程基本和之前的…

智能故障诊断期刊推荐【中文期刊】

控制与决策 http://kzyjc.alljournals.cn/kzyjc/home 兵工学报 http://www.co-journal.com/CN/1000-1093/home.shtml 计算机集成制造系统 http://jsjjc.soripan.net/ 机械工程学报 http://www.cjmenet.com.cn/CN/0577-6686/home.shtml 太阳能学报 https://www.tynxb.org.c…

Visual Studio Code中的任务配置文件tasks.json中的可选任务组tasks详解

☞ ░ 前往老猿Python博客 ░ https://blog.csdn.net/LaoYuanPython 一、引言 vscode是支持通过配置可以实现类似Visual C等IDE开发工具使用菜单和快捷键直接进行程序编译构建的&#xff0c;这样构建的任务可以结合后续的调试配置进行IDE环境的程序调试&#xff0c;不过在之前…

12. IO

1.File类 • File 类代表与平台无关的文件和目录。 • File 能新建、删除、重命名文件和目录&#xff0c;但 File 不能访问文件内容本身。如果需要访问文件内容本身&#xff0c;则需要使用输入/输出流。 1).File的常用方法 在这里插入图片描述 2).遍历给定目录所有文件 …