【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;设备最多可…

​subprocess --- 子进程管理​

源代码: Lib/subprocess.py subprocess 模块允许你生成新的进程&#xff0c;连接它们的输入、输出、错误管道&#xff0c;并且获取它们的返回码。此模块打算代替一些老旧的模块与功能&#xff1a; os.system os.spawn*在下面的段落中&#xff0c;你可以找到关于 subprocess 模…

代理模式

接口 public interface UserService {void selectAll(); }实现类&#xff08;需要增加业务&#xff09; public class UserServiceImpl implements UserService{Overridepublic void selectAll() {System.out.println("查询");} }静态代理 代理类 public class Us…

选择适合微服务的编程语言

关注公众号【爱发白日梦的后端】分享技术干货、读书笔记、开源项目、实战经验、高效开发工具等&#xff0c;您的关注将是我的更新动力&#xff01; 讨论编程语言就像是一场政治辩论。每个开发者都会过分捍卫他/她所使用的编程语言。然而&#xff0c;编程语言应该被看作是它们真…

Tofu目标识别跟踪模块

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

IPv6网络协议有什么用

IPv6是英文“Internet Protocol Version 6”&#xff08;互联网协议第6版&#xff09;的缩写&#xff0c;是互联网工程任务组&#xff08;IETF&#xff09;设计的用于替代IPv4的下一代IP协议&#xff0c;号称可以为全世界的每一粒沙子编上一个地址。IPv6的使用&#xff0c;不仅…

一起看看StatusBarManagerService(三)

写在前面 StatusBarManagerService中API涉及systemui的多个模块&#xff1b;本篇主要介绍StatusBarManagerService中与通知栏相关的API和几个通用API。 因为我对系统UI了解的不全&#xff0c;其他API暂不整理&#xff0c;怕误人子弟。。 通知栏相关函数解析 1.展开通知栏 vo…

智慧灯杆技术应用分析

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

如何测试Nginx防盗链是否生效?

1、查看Nginx防盗链规则是否正确 打开Nginx的配置文件&#xff0c;找到防盗链规则。一般Nginx防盗链的规则内容大致如下&#xff1a; location ~* \.(jpg|jpeg|png|gif)$ {valid_referers none blocked example.com;if ($invalid_referer) {return 403;}} 上述配置会拦截所…

利用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)…

c++标识线程

c标识线程 线程ID类型为std::thread::id&#xff0c;它有两种方式获取。 直接通过std::thread对象的成员函数get_id()来获取。如果thread对象没有与任何执行线程相关联&#xff0c;get_id()将返回std::thread::id对象&#xff0c;它按照默认的构造方式生成&#xff0c;表示线程…

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

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

『OPEN3D』1.8.3 多份点云配准

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

orcad(Cadence)常用库olb介绍

基础库&#xff1a; CAPSYM.OLB capsym 共35个零件&#xff0c;存放电源&#xff0c;地&#xff0c;输入输出口&#xff0c;标题栏等。DISCRETE.OLB 共872个零件&#xff0c;存放分立式元件&#xff0c;如电阻&#xff0c;电容&#xff0c;电感&#xff0c;开关&#xff0c;变…

DHCP—动态主机配置协议

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

pom配置文件重要标签探究

文章目录 dependencies标签dependencyManagement标签两者辨析repositories标签properties标签 dependencies标签 <dependencies>标签用于指定项目的依赖项列表。这些依赖项可以是应用程序代码所需的库&#xff0c;也可以是Spring Boot和其他第三方库。<dependencies&…

RocketMQ 总体概括

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

打家劫舍Ⅱ java

题目描述 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋&#xff0c;每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 &#xff0c;这意味着第一个房屋和最后一个房屋是紧挨着的。同时&#xff0c;相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的…

深入解析MySQL中内连接、外连接的区别及实践应用

​嗨&#xff0c;大家好&#xff0c;欢迎来到程序猿漠然公众号&#xff0c;我是漠然。在数据库查询中&#xff0c;连接是一种常用的操作&#xff0c;用于从两个或多个表中获取数据。本文将详细介绍MySQL中的内连接、外连接的概念、区别以及实践应用&#xff0c;帮助大家更好地理…