Java 中 synchronized 的优化操作:锁升级、锁消除、锁粗化

由 并发编程中常见的锁策略 总结可知,synchronized 具有以下几个特性:

  1. 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁。
  2. 开始是轻量级锁实现,如果锁被持有的时间较长,就转换成重量级锁。
  3. 实现轻量级锁时,大概率用自旋锁策略。
  4. 是一种不公平锁。
  5. 是一种可重入锁。
  6. 不是读写锁。

本文介绍synchronized的几种优化操作,包括锁升级、锁消除和锁粗化。

一、锁升级

JVM 将 synchronized 锁分为无锁、偏向锁、轻量级锁、重量级锁这四种状态。在加锁过程中,会根据实际情况,依次进行升级。(**目前主流的 JVM 的实现,只能锁升级,不能锁降级!**不是无法实现,只不过可能是因为存在一些代价,使得这样做的收益和代价不成比例,因此就没有实现。)

整体的加锁过程(锁升级过程):刚开始加锁,是偏向锁状态;遇到锁竞争后,升级成自旋锁(轻量级锁);当竞争更激烈时,就会变成重量级锁(交给内核阻塞等待)。

1、偏向锁(Biased Locking)

第一个尝试加锁的线程优先进入偏向锁状态。偏向锁是Java虚拟机(JVM)中用于提高线程同步性能的一种优化技术。在多线程环境中,对共享资源进行同步操作,需要使用锁(synchronized)来保证线程的互斥访问。传统的锁机制存在竞争和上下文切换的开销,对性能会有一定的影响。而偏向锁则是为了减少无竞争情况下的锁操作开销而引入的。

偏向锁不是真的“加锁”,只是先让线程针对锁对象有个标记,记录某个锁属于哪个线程。

它的基本思想是,当一个线程获取锁并访问同步代码块时,如果没有竞争,那么下次该线程再次进入同步块时,无需再次获取锁。这是因为在无竞争的情况下,假设一个线程反复访问同步代码块,无需每次都去竞争锁,只需判断锁是否处于偏向状态;如果是,那么直接进入同步代码块即可。

通俗来说就是,如果后续没有其他线程再来竞争该锁,那么就不用真的加锁了,从而避免了加锁解锁的开销。 但一旦还有其他线程来尝试竞争这个锁,偏向锁就立即升级成真的锁(轻量级锁),此时别的线程就只能等待了。这样做既保证了效率,也保证了线程安全。

如何判定有没有别的线程来竞争该锁?

注意,偏向锁是synchronized内部做的工作。synchronized会针对某个对象进行加锁,这个所谓的“偏向锁”正是在这个对象里头做一个标记。

由于一开始已经在锁对象中记录了当前锁属于哪个线程,因此很容易识别当前申请锁的线程是否是一开始就记录了的线程。

如果另一个线程正在尝试对同一个对象进行加锁,也会先尝试做标记,但结果却发现已经有标记了。于是JVM就会通知先来的线程,让它赶快把锁升级一下。

偏向锁本质上是“延迟加锁”,即能不加锁就不加锁,尽量避免不必要的加锁开销;但是该做的标记还是得做的,否则就无法区分何时需要真正加锁。

举个栗子理解偏向锁

假设男主是一个锁,女主是一个线程。如果只有女主和男主暧昧(即只有这一个线程来使用这个锁),那么即使男主和女主不领证结婚(避免了高成本操作),也可以一直生活下去。

但是如果此时有女配出现,也尝试竞争男主,想和男主搞暧昧,那么此时女主就必须当机立断,不管领证结婚这个操作成本多高,也势必要把这个动作完成(即真正加锁),让女配死心。

所以说,偏向锁 = 搞暧昧~~

2、自旋锁

**什么是自旋锁?**在锁策略的文章中提到:

自旋锁是一种典型的轻量级锁的实现方式,它通常是纯用户态的,不需要经过内核态。按之前的方式,线程在抢锁失败后即进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但实际上,在大部分情况下虽然当前抢锁失败,但过不了很久锁就会被释放,没必要就放弃 CPU。这个时候就可以使用自旋锁来处理这样的问题。

自旋锁是一种忙等待锁的机制。当一个线程需要获取自旋锁时,它会反复地检查锁是否可用,而不是立即被阻塞。如果获取锁失败(锁已经被其他线程占用),当前线程会立即再尝试获取锁,不断自旋(空转)等待锁的释放,直到获取到锁为止。第一次获取锁失败,第二次的尝试会在极短的时间内到来。这样能保证一旦锁被其他线程释放,当前线程能第一时间获取到锁。

优点:没有放弃 CPU,不涉及线程阻塞和调度。一旦锁被释放就能第一时间获取到锁。
缺点:如果锁被其他线程持有的时间比较久,那么就会持续的消耗 CPU 资源(忙等),而挂起等待的时候是不消耗 CPU 的。

自旋锁适用于保护临界区较小、锁占用时间短的情况,因为自旋会消耗CPU资源。自旋锁通常使用原子操作或特殊的硬件指令来实现。

随着其他线程进入锁竞争,偏向锁状态会被消除,进入轻量级锁状态,即自适应的自旋锁。

此处的轻量级锁是通过 CAS 来实现。通过 CAS 检查并更新一块内存 (比如比较 null 与该线程引用是否相等),如果更新成功,则认为加锁成功;如果更新失败,则认为锁被占用,继续自旋式的等待,期间并不放弃 CPU 资源。

(见 详解CAS算法)

CAS算法实现自旋锁的原理

由于自旋操作是一直让 CPU 空转,比较浪费 CPU 资源,因此此处的自旋不会一直持续进行,而是达到一定的时间或重试次数就不再自旋了。这也就是所谓的 “自适应”。

3、重量级锁

**什么是重量级锁 ?**在锁策略的文章中提到:

简单来说,轻量级锁是加锁解锁的过程更快更高效的锁策略,而重量级锁是加锁解锁的过程更慢更低效的锁策略。重量级锁中加锁机制重度依赖 OS 提供的 mutex(互斥量)。

  • 大量的内核态用户态切换。
  • 很容易引发线程的调度。

这两个操作的成本都比较高,而且一旦涉及到用户态和内核态的切换,效率就低了。

如果竞争进一步激烈,自旋不能快速获取到锁状态。就会膨胀为重量级锁。

自旋锁虽然能最快获取到锁,但是要消耗大量 CPU(因为自旋的时候CPU是快速空转的)。如果当前锁竞争非常激烈,比如 50 个线程竞争一个锁,1 个争上,另外 49 个等待。这么多线程都在自旋空转,CPU的消耗就非常大。既然如此,就更改锁策略,升级成重量级锁,让其它的线程都在内核里进行阻塞等待(这意味着线程要暂时放弃 CPU 资源,由内核进行后续调度)。

(PS:目前的主流操作系统如 windows,Linux,调度的开销都是很大的。系统不承诺能在 xx 时间内一定能完成指定的调度,极端情况下调度的开销可能非常大。

但还存在另外一种实时操作系统(例如 vxworks),它能够以更低的成本完成任务调度,但牺牲了更多的其他功能。在如火箭发射这种对时间精度比较高的特殊领域就会用到。)

如果竞争进一步激烈,自旋不能快速获取到锁状态。就会膨胀为重量级锁。

此处的重量级锁就是指内核提供的 mutex 。

  1. 某线程执行加锁操作,先进入内核态。
  2. 在内核态判定当前锁是否已经被别的线程占用 。
  3. 如果该锁没有占用,则加锁成功,并切换回用户态。
  4. 如果该锁被占用,则加锁失败。此时线程进入锁的等待队列并挂起,等待被操作系统唤醒。
  5. 经历了一系列的“沧海桑田”,这个锁终于被其他线程释放了,此时操作系统也想起了这个被挂起的线程,于是唤醒这个线程,并让它尝试重新获取锁。

二、锁消除

锁消除也是“非必要,不加锁”的一种体现。与锁升级不同,锁升级是程序在运行阶段 JVM 做出的优化手段。而锁消除是在程序编译阶段的优化手段。编译器和 JVM 会检测当前代码是否是多线程执行或是否有必要加锁。如果无必要,但又把锁给写了,那么在编译的过程中就会自动把锁去掉。

有些应用程序代码中可能会用到没有必要用到的 synchronized。例如 StringBuffer 就是线程安全的,它的每一个关键方法都加了synchronized关键字:

StringBuffer的部分源码

但这里就有一个问题:如果是在单线程中使用StringBuffer,是不涉及线程安全问题的。这个时候其实就没必要加锁。那么这时编译器就会出手,发现synchronized是没必要加的,就会在编译阶段把synchronized去掉,相当于加锁操作没有真正被编译。

StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");

此时,每个 append 的调用都会涉及加锁和解锁。但如果只是在单线程中执行这段代码,那么其中的这些加锁解锁操作是没有必要的,白白浪费了一些资源开销。

锁消除整体来说是一个比较保守的优化手段,毕竟编译器肯定得保证消除的操作是靠谱的。所以只有十拿九稳的时候才会实施锁消除,否则仍然会上锁,这时就会交给其它的操作策略来对锁进行优化(比如上面的锁升级)。

三、锁粗化

锁的粒度指的是 synchronized 代码块中包含代码的多少。代码越多,粒度越大;代码越少,粒度越小。

一般我们在写代码时,多数情况下是希望锁的粒度更小一点。(锁的粒度小就意味着串行执行的代码更少,并发执行的代码更多)。如果某个场景需要频繁地加锁解锁,此时编译器就可能把这个操作优化成个粒度更粗的锁,即锁的粗化。

实际开发过程中使用细粒度锁,是期望释放锁的时候其他线程能使用锁。但是实际中可能并没有其他线程来抢占这个锁。这种情况 JVM 就会自动把锁粗化,避免频繁申请释放锁造成不必要的开销。

举个栗子理解锁粗化

上班时要向领导汇报工作。你的领导给你安排了三个工作:A、B、C。
汇报方式有:

  1. 先打个电话,汇报工作 A 的进展,挂了电话;再打个电话,汇报工作 B 的进展,挂了电话;再打个电话,汇报工作C的进展,挂了电话。(你给领导打电话,领导接你的电话,领导就干不了别的;别人要给领导打电话,就只能阻塞等待。每次锁竞争都可能引入一定的等待开销,此时整体的效率可能反而更低。)
  2. 打个电话,一口气汇报 工作 A,工作B,工作 C,挂了电话。

显然第二种方式是更加高效的。

可见,synchronized 的策略是比较复杂的,它是一个很“智能”的锁。

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

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

相关文章

Spring Cloud Alibaba【Nacos配置动态刷新、Nacos集群架构介绍 、Nacos的数据持久化、认识分布式流量防护 】(五)

目录 分布式配置中心_Nacos配置动态刷新 分布式配置中心_Dubbo服务对接分布式配置中心 分布式配置中心_Nacos集群架构介绍 分布式配置中心_Nacos的数据持久化 分布式配置中心_Nacos集群配置 分布式流量防护_认识分布式流量防护 分布式流量防护_认识Sentinel 分布式配置…

pycharm里debug时torch数组显示不全

pycharm里查看torch数组全部值 一、在Pycharm运行torch数组时,通常只能看到数组的一部分二、解决办法1、debug后,鼠标右键想要查看完整的数组,选择Evaluate Expression2、输入np.array(x0.data),x0为想要查看的数组名,…

在Linux下做性能分析1:基本模型

介绍 本Blog开始介绍一下在Linux分析性能瓶颈的基本方法。主要围绕一个基本的分析模型,介绍perf和ftrace的使用技巧,然后东一扒子,西一扒子,逮到什么说什么,也不一定会严谨。主要是把这个领域的一些思路和技巧串起来。…

Flutter悬浮UI的设计Overlay组件

文章目录 APP开发经常要遇到的开发场景Overlay 的介绍Overlay的使用规则举例说明源码例子报错报错No Overlay widget found报错原因解决方法 修改后的源码 例子效果 APP开发经常要遇到的开发场景 有时候我们在开发APP的时候会遇到下面这些需求: 在现有页面上添加浮…

代码随想录算法训练营第十九天 | 动态规划系列5,6,7,8

动态规划系列5,6,7,8 377 组合总和 Ⅳ未看解答自己编写的青春版重点代码随想录的代码我的代码(当天晚上理解后自己编写)求排列数的题,用二维DP过不了?自己捋逻辑的话,也是可以觉得有漏洞,但是怎么修改,一下子还没思路&…

JVM之内存与垃圾回收篇2

文章目录 3 运行时区域3.1 本地方法栈3.2 程序计数器3.3 方法区3.3.1 Hotspot中方法区的演进3.3.2 设置方法区内存大小3.3.3 运行时常量池3.3.4 方法区使用举例3.3.5 方法区的演进3.3.5 方法区的垃圾回收 3.4 栈3.4.1 几个面试题 3.5 堆3.5.1 Minor GC、Major GC和Full GC3.5.2…

适配器模式-不兼容结构的协调

去英语国家旅游时,我们只会说中文,为了与当地人交流,我们需要购买个翻译器,将中文翻译成英文,而这运用了适配器模式。 1 概述 适配器模式(Adapter Pattern),将一个接口转换成客户喜…

idea-控制台输出乱码问题

idea-控制台输出乱码问题 现象描述: 今天在进行IDEA开发WEB工程调式的时候控制台日志输出了乱码,如下截图 其实开发者大多都知道乱码是 编码不一致导致的,但是有时候就是不知到哪些地方不一致,今天我碰到的情况可能和你的不相同…

APACHE KAFKA本机Hello World教程

目标 最近想要简单了解一下Apache Kafka,故需要在本机简单打个Kafka弄一弄Hello World级别的步骤。 高手Kafka大佬们,请忽略这里的内容。 步骤 Apacha Kafka要求按照Javak8以上版本的环境。从官网下载kafka并解压。 启动 # 生产kafka集群随机ID KA…

未来Mac下载站怎么打不开了

重要公告: 未来软件园因业务需要现更换域名 原域名:Mac.orsoon.com 更为新域名:未来mac下载-Mac软件-mac软件下载-mac软件大全 程序已全面转移,请访问新域名

软件基础问答题

性能: 负载压力测试是指在一定约束条件下测试系统所能承受的并发用户量、运行时间、数据量等,以确定系统所能承受的最大负载压力。 负载测试是通过逐步增加系统负载,测试系统性能的变化,并最终确定在满足性能指标的情况下&#xf…

【技能实训】DMS数据挖掘项目-Day11

文章目录 任务12【任务12.1】创建用户信息表【任务12.2】在com.qst.dms.entity下创建用户实体类User,以便封装用户数据【任务12.3】在com.qst.dms.service下创建用户业务类UserService【任务12.4】在项目根目录下创建图片文件夹images,存储dms.png【任务…

计算机毕业论文选题推荐|软件工程|信息管理|数据分析|系列一

文章目录 导文题目导文 计算机毕业论文选题推荐|软件工程|信息管理 (***语言)==使用其他任何编程语言 例如:基于(***语言)门窗账务管理系统的设计与实现 得到:基于JAVA门窗账务管理系统的设计与实现 基于vue门窗账务管理系统的设计与实现 等等 题目 基于requests多线程…

微信小程序——字符串截取

indexOf() : 判断一个字符是否在字符串 中 存在,如果存在返回该元素或字符第一次出现 的 位置 的 索引,不存在返回-1。 lastIndexOf() : 返回一个指定的字符串值最后出现的位置,在一个字符串中的指定位置从后向前搜索。…

静态数码管显示

学习芯片: EP4CE6F17C8 本次学习使用的为共阴极数码管,即用低电平点亮数码管,同样可知,共阳极数码管的阳极连在一起,即用高电平点亮数码管。 八段数码管示意图: a,b,c,d,e,f,g,dg表示八段数码管时&#…

7、sentinel使用和源码分析

一、分布式系统遇到的问题 1、服务雪崩效应 在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用。如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等待&#x…

力扣 135. 分发糖果

题目来源:https://leetcode.cn/problems/candy/description/ C题解(来源代码随想录): 先从左往右比较,右边孩子评分比左边高就多发1颗糖,否则就只发1颗;再从右往左比较,左边孩子评分…

机械臂的雅克比矩阵推导

1. 线速度和角速度的递推通式推导 p i p i − 1 R i − 1 r i − 1 , i i − 1 \mathbf{p}_{i}\mathbf{p}_{i-1}\mathbf{R}_{i-1} \mathbf{r}_{i-1, i}^{i-1} pi​pi−1​Ri−1​ri−1,ii−1​ p i − 1 \mathbf{p}_{i-1} pi−1​是 { i − 1 } \{i-1\} {i−1}坐标系的原点的…

记一次ruoyi中使用Quartz实现定时任务

一、首先了解一下Quartz Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的程序。Jobs可以做成标…

UE4/5AI制作基础AI跳跃(适合新手)

目录 制作 添加逻辑 添加导航链接代理 结果 在上一章中,我们讲解了简单的AI跟随玩家,制作了一个基础的ai。 UE4/5AI制作基础AI(适合新手入门,运用黑板,行为树,ai控制器,角色类,任…