Java 多线程(八)—— 锁策略,synchronized 的优化,JVM 与编译器的锁优化,ReentrantLock,CAS

前言

本文为 Java 面试小八股,一句话,理解性记忆,不能理解就死背吧。

锁策略

悲观锁与乐观锁

悲观锁和乐观锁是锁的特性,并不是特指某个具体的锁。

我们知道在多线程中,锁是会被竞争的,悲观锁就是指锁的竞争程度十分激烈,很多线程都想用这把锁,为了应对这个场景,我们会额外做一些工作。例如:一把锁,此时有几十个线程都想用,并且同一时刻它们都发出申请锁的请求,这时候锁的竞争程度很高,我们可以采取悲观锁的策略,额外做一些工作。

乐观锁则相反,锁的竞争程度很小,就不需要做额外的工作。例如:这一把锁只有两个线程在竞争,并且这两个线程用锁的概率也不是很高,这时候我们可以采用乐观锁策略。

重量级锁与轻量级锁

重量级锁和轻量级锁是遇到特定的场景而出现的解决方案。

上面我们就提到乐观和悲观的场景,重量级锁适用于悲观的场景,相应的也要付出更高的代价,效率相比轻量级锁要低效。

轻量级锁适用于乐观的场景,要付出的代价也要小很多,效率相比重量级锁要高效。

等待挂起锁与自旋锁

挂起等待锁就是指如果一把锁已经被一个线程占用的时候,发现有其他线程还想竞争这把锁,操作系统就会让它们阻塞等待,后续唤醒的时候需要由操作系统的内核来唤醒。

自旋锁就是指如果发现由锁竞争,这时候这些线程不会阻塞等待,而是以忙等的形式进行等待。

看到这里,其实等待挂起锁是适用于悲观的场景下,因为线程竞争激烈,没必要让它们占着 CPU 资源,直接让它们阻塞,释放出CPU 资源,减少资源的消耗。同时由于唤醒的时候是由操作系统的内核实现的,所以操作系统会在内核态和用户态频繁切换,效率也会比较低下。

而自旋锁则适用于乐观的场景,线程以忙等的形式,也就是占用着CPU,但是由于锁竞争不是很激烈,忙等的线程很快就可以获取到锁,所以没必要阻塞等待,因为操作系统的内核唤醒线程的效率要低效一些,所以自旋锁的效率会比等待挂起锁的效率要高。

互斥锁与读写锁

互斥锁就是加锁之后,拥有这把锁的线程才能进行操作,其他线程必须等待拿到锁之后才能进行自己的操作,这就是互斥。

读写锁分为读锁、写锁,因为我们知道线程安全问题是因为写操作而引起的,但是读操作是不会发生线程安全问题的,而读写锁就是针对读操作和写操作进行加锁读锁和读锁是不会互斥的,写锁与读锁是会互斥的,写锁与写锁是会互斥的(这里可以参考MySQL的幻读、脏读、不可重复读了)

公平锁与非公平锁

公平锁是指锁的分配是按线程的等待时长来分配的,举个例子:假设一把锁已经被一个线程占用,此时有三个线程都想竞争这把锁,那这时候我们会使用额外的数据结构来保存这些线程并且记录每个线程的等待时长,等到锁被释放的时候,操作系统会优先把这把锁分配给等待时长最长的线程,这也避免了线程饥饿。

非公平锁就是随机分配,不按 “先来先得” 的规矩

可重入锁与不可重入锁

可重入锁就是当一个线程拥有这把锁的时候,可以进行重复的加锁。

不可重入锁则相反,即使你这个线程拥有了这把锁,但是还是不能对其进行重复加锁。

synchronized 的优化

根据上面的锁策略,我们来总结一下 synchronized 的特性:

synchronized 具有自适应性
synchronzied 开始时是采取乐观锁策略,如果锁的冲突频繁,则转换为悲观锁
开始时是轻量级锁,如果锁冲突频繁,则转换为重量级锁
synchronized 实现轻量级锁的时候采用自旋锁策略
synchronized 是不公平锁,可重入锁,互斥锁,不是读写锁

锁升级

JVM 会将 synchronized 的锁分为四个状态:无锁、偏向锁、自旋锁、重量级锁。

在这里插入图片描述

当还没进入synchronized 的时候,处于无锁状态,一旦进入 synchronized 代码块就会变成偏向锁,偏向锁并非真正加锁,而是通过标记的方式,以此来区分是否真正加锁了。偏向锁本质上相当于 “延迟加锁”,能不加锁就不加锁,避免了不必要的加锁开销,这也是一种懒汉模式的体现。

一旦产生锁竞争,偏向锁就会升级为自旋锁,也就是轻量级锁,如果竞争十分激烈,进一步升级为重量级锁。

synchronized 只能进行锁升级,但是不能进行锁降级!!!

JVM 与编译器的锁优化

锁消除

JVM 会自动检测出一些没有必要加锁的操作,避免这些无意义的加锁操作带来的不必要的开销,JVM 会把这些锁给消除,也就是说你代码加锁了,但是 JVM 给删除了。

大家不用担心这个优化会产生线程安全问题,因为 JVM 的锁消除是在100% 确定这个锁就是一个没必要加的锁,JVM 才会进行锁消除。

锁粗化

首先介绍一个概念,锁的粒度:加锁与解锁之间包含的代码指令越多,锁就越粗;相反,加锁与解锁之间包含的代码指令越少,锁就越细。

public class Test {public static int sum = 0;public static int count = 10000;public static int total = 1000;public static void main(String[] args) {Object locker = new Object();Thread t = new Thread(() -> {for(int i = 0; i < 5000; i++) {synchronized (locker) {sum++;}synchronized (locker) {count--;}synchronized (locker) {total--;}}});}
}

上面的代码就属于锁的粒度太细了,频繁加锁解锁。

        Thread t2 = new Thread(() -> {for(int i = 0; i < 5000; i++) {synchronized (locker) {sum++;count--;total--;}}});

这个代码就是锁的粒度粗,加锁和解锁的次数比较少。

⼀段逻辑中如果出现多次加锁解锁,编译器 和 JVM会自动进行锁的粗化。

ReentrantLock

ReentrantLock 和 synchronized 是并列关系,都是用来加锁的。

简单使用介绍,ReentrantLock 使用 lock() 加锁,unlock() 来解锁,为了避免我们因为加锁和解锁之间有return 或者 抛出异常等等情形没能进入解锁操作,所以这里使用 finally 来包含 unlock() 代码行,避免忘记解锁。

        ReentrantLock locker2 = new ReentrantLock();Thread t3 = new Thread(() -> {try {locker2.lock();count++;} finally {locker2.unlock();}   });

synchroinzed 和 ReentrantLock 的区别:
synchronized 是 Java提供的关键字,是 JVM 内部通过 C++ 实现的,ReentrantLock 是Java标准库提供的类,由Java代码实现
synchronized 是 通过代码块来实现加锁和解锁的,ReentrantLock 通过 lock() 加锁,unlock() 解锁,一定要注意 unlock() 可能未被调用的情况。
ReentrantLock 还有一个 tryLock() 这个方法的调用不会线程产生阻塞,如果加锁成功则返回 true,加锁失败则返回 false,接下来由调用者来根据返回值决定接下来怎么做。可以设置超时时间,当等待时间达到超时时间的时候再返回true / false
ReentrantLock 提供了公平锁的实现,ReentrantLock locker = new ReentrantLock(true);默认情况下是非公平锁。
在这里插入图片描述
ReentrantLock 搭配的通知等待机制是由Condition 类实现的,相比于 synchronized 的 wait / notify 的功能更强大一些。

CAS

CAS 全称是 Compare and swap,比较并交换

CAS 在 CPU 里是一条指令,具有原子性。
因此 CAS 操作是线程安全的

举个例子:假设内存原始数据为 V,把这个数据放入寄存器 1 和 寄存器 2 中,数据的加减等操作的结果由寄存器 2 保存。CAS 会先检测原始数据 V 和寄存器 1 的数值是否一致,如果一致的话,可以执行修改也就是把寄存器 2 的结果放入内存中。

下面给出 CAS 的伪代码进行进一步的理解:

boolean CAS(address, expectValue, swapValue) {if (&address == expectedValue) {&address = swapValue;return true;}return false;
}

第一个参数是内存的数值,第二个参数是寄存器 1 的数值,第三个参数是寄存器 2 的数值。

首先判断内存的数值是否和寄存器的数值一致,如果一致则进行寄存器 2 和内存数值的交换操作,注意 这本质上在 CPU 里是一条指令,具有原子性。

明确的指明:if-else 和 三目运算符在 CPU 里不是一条指令,和 CAS 还是由区别的。

原子类

CPU 有 CAS 指令,并且给操作系统提供了 CAS 的使用接口,操作系统对 CAS 进一步封装,给用户提供相应的接口,C++ 可以直接进行调用,而JVM 是由 C++ 实现的再次对 CAS 进行封装,给Java 程序员提供了 原子类。在这个 java.util.concurrent.atomic 包下就是我们的原子类了。

在这里插入图片描述

下面是原子类的伪代码:

class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while ( CAS(value, oldValue, oldValue+1) != true) {oldValue = value;}return oldValue;}
}

oldValue 是寄存器,由于Java没有寄存器的使用,所以这里用 int 类型代替。

getAndIncrement() 其实就是 ++ 自增的操作,首先先把内存的数值(value)读到寄存器 1 中,CAS 指令 首先判断 value 是否和寄存器 1 中的数值 oldValue 相等,如果相等就把寄存器 2 的 oldValue + 1 的结果放到内存中,返回 true,否则返回 false 并且进入循环体再次读取内存的数值放入寄存器 1 中。


面对多线程下同时修改一个变量的时候,原子类是最佳的选择。

import java.util.concurrent.atomic.AtomicInteger;public class Demo1 {private static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count.getAndIncrement();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + count.get());}
}

在这里插入图片描述

使用 CAS 实现自旋锁

下面是 使用 CAS 实现自旋锁的伪代码:

public class SpinLock {private Thread owner = null;public void lock(){// 通过 CAS 看当前锁是否被某个线程持有.  // 如果这个锁已经被别的线程持有, 那么就⾃旋等待.  // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.  while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock (){this.owner = null;}
}

核心代码:while(!CAS(this.owner, null, Thread.currentThread())){} 当 这个锁的拥有者 为 null 的时候,才能由线程Thread.currentThread() 获取这把锁的操作并返回true,否则该线程以忙等的形式等待这把锁。

ABA 问题

ABA 问题是什么?
我们知道 CAS 开始之前会先把内存的数值读到寄存器里,在进行 CAS 的操作之前,可能调度过来别的线程,这个线程对这个内存的数值进行了修改操作,然后又改回来了,看上去这个数值没有任何变化,实际上这个数据已经被动过了,接着把 CAS 调度过来执行,CAS 首先判定内存的数值是否和寄存器的数值一致,如果一致,进行交换操作,这时候数值肯定是一致的,所以交换操作正常被执行了。在进行内存数值和寄存器数值判定是否相等之前内存数值是否被改了又改过,这就是 ABA 问题。

ABA 问题会带来什么BUG?
假设一个人叫做白糖过来取500块钱,假设余额有 4k,这时候 ATM 机有点卡顿,这时候白糖进行了多次按下取款的操作,恰好这时候白糖的好朋友天王星发个信息说之前欠你的500块现在转账还你。
由于多次按下取款操作,就会产生多个取款的线程来执行取款操作,此时中间夹了一个还款操作的线程,大家来看一下下面的流程图:
在这里插入图片描述
取款线程 t1 把 account 修改为 3500, 还款线程将 account 修改为 4000, 接着又来了 取款线程 t3 由于内存3500 和寄存器的数值一致,所以将余额修改为了 3500,你会发现白糖小伙就拿出了 500 块,但是余额却多扣了 500, 完了血亏 500,可怜的白糖又要辛苦打工了。

这种事件虽然发生概率极小,但是在庞大的请求数量面前还是不能忽视这个 bug 的。

如何解决 ABA 问题???
因为余额是可以加又可以减的变量,所以会出现上述极端的BUG,但是如果我们换一个指标来作为判断标准的话就可以避免上述的BUG,这里我们可以使用版本号来作为判断的指标,每次修改之后版本号就 + 1,每次进行修改操作的时候判断内存的版本号和寄存器的版本号是否相同

下面给一个伪代码:

        int oldVersion = version;if(CAS(version, oldVersion, oldVersion + 1)) {account += 500;}

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

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

相关文章

LSTM反向传播及公式推导

先回顾一下正向传播的公式: 化简一下: 反向传播从下到上逐步求偏导: 对zt求偏导(预测值和标签值相减): zt对未知数wt,ht,bt分别求偏导: ht对ot,Ct求偏导: ot对Net0求偏导: Net0对w0,b0求偏导: .... 总体的思路就是那个公式从下到上逐步对未知数求偏导: 下面是总体的流程…

Flutter项目打包ios, Xcode 发布报错 Module‘flutter barcode_scanner‘not found

报错图片 背景 flutter 开发的 apple app 需要发布新版本&#xff0c;但是最后一哆嗦碰到个报错&#xff0c;这个小问题卡住了我一天&#xff0c;之间的埪就不说了&#xff0c;直接说我是怎么解决的&#xff0c;满满干货 思路 这个报错 涉及到 flutter_barcode_scanner; 所…

微信小程序性能优化 ==== 合理使用 setData 纯数据字段

目录 1. setData 的流程 2. 数据通信 3. 使用建议 3.1 data 应只包括渲染相关的数据 3.2 控制 setData 的频率 3.3 选择合适的 setData 范围 3.4 setData 应只传发生变化的数据 3.5 控制后台态页面的 setData 纯数据字段 组件数据中的纯数据字段 组件属性中的纯数据…

Java.6--多态-设计模式-抽象父类-抽象方法

一、多态 1.定义--什么是多态&#xff1f; a.同一个父类的不同子类对象&#xff0c;在做同一行为的时候&#xff0c;有不同的表现形式&#xff0c;这就是多态。&#xff08;总结为&#xff1a;一个父类下的不同子类&#xff0c;同一行为&#xff0c;不同表现形式。&#xff0…

springboot3.x.x 集成 连接SQL Server 2008 驱动版本和SSL套接字问题的解决

驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立安全连接。错误:“The server selected protocol version TLS10 is not accepted by client 依赖版本 <dependency><groupId>com.microsoft.sqlserver</groupId><artifactId>mssql-jdbc&…

ABAP 函数

1、基础语句注意事项 1.1基础 SE38编辑 SM30 数据库表中添加多条数据 SE91编辑消息类 SE11查看数据库表 SE16N主要查看数据 1.2语句 1.FOR ALL ENTRIES IN 对于不能使用join的聚集表或者需要使用SELECT 的内表&#xff0c;我们一般使用for all entries in 语句将该表…

虚拟机安装麒麟v10、配置网络、安装docker

一、虚拟机安装麒麟v10 1、下载iso&#xff08;https://www.kylinos.cn/support/trial.html&#xff09; 2、虚拟机安装 3、选择 4、设置开机自动连接网络 参考地址&#xff1a;https://www.cnblogs.com/goding/p/18283912 安装好后发现屏幕分辨率毕竟低&#xff0c;点设置分…

开源模型应用落地-LangChain实用小技巧-带阈值的相似性搜索(十五)

一、前言 带阈值的相似性搜索是一种非常实用的信息检索方法。它允许用户设定一个具体的相似度标准&#xff0c;从而提升搜索结果的相关性和准确性。在面对大规模数据时&#xff0c;传统的相似性搜索往往难以满足用户的需求&#xff0c;因为返回的结果可能包含很多不相干的信息。…

数字图像处理的概念(一)

一 何谓数字图像处理 1 图像的概念 图像是对客观存在的物体的一种相似性的、生动的写真或描述。 2 图像的类别 可见光成像和不可见光成像 单波段、多波段和超波段图像 伽马射线成像 主要用途包括核 医学和天文观测 等 。 核医学 a)同位素注射 骨骼扫描图像 b)正电子放射( …

华为“纯血鸿蒙”重磅发布!首次融入原生AI能力

华为成功举办“原生鸿蒙之夜暨全场景新品发布会”&#xff0c;会上隆重宣布&#xff1a;我国首个自主研发的移动操作系统——华为原生鸿蒙操作系统HarmonyOS NEXT正式面世&#xff0c;标志着鸿蒙历史上的一次重大飞跃&#xff0c;至此&#xff0c;鸿蒙系统已成为继苹果iOS与安卓…

算法革新决定未来!深挖数字北极星3.0背后的技术逻辑

2023年9月22日,望繁信科技首届PRO_大会在广州圆满举行,望繁信科技联合创始人&CTO李进峰博士在大会主论坛带来了《再突破——流程挖掘算法的革新和调优》的精彩分享,向大家详细介绍了数字北极星3.0以及流程资产等新功能背后的核心技术。 创新流程资产挖掘图算法 为中国企…

Flutter 状态管理框架Get

状态管理框架 Get的使用 目录 状态管理框架 Get的使用 GetMaterialApp 路由的注册 路由的跳转 middlewares的使用 组件使用 defaultDialog bottomSheet snackbar 状态刷新有很多种方式 ValueBuilder Obx 基础使用 是时候引入GetxController, 也是Get里面的常用的 G…

OpenCV与AI深度学习 | 实战 | OpenCV中更稳更快的找圆方法--EdgeDrawing使用演示(详细步骤 + 代码)

本文来源公众号“OpenCV与AI深度学习”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;实战 | OpenCV中更稳更快的找圆方法--EdgeDrawing使用演示&#xff08;详细步骤 代码&#xff09; 导 读 本文主要介绍如何在OpenCV中使用E…

pycharm 中提示ModuleNotFoundError: No module named ‘distutils‘

在Pycharm 中的命令行中输入 pip install setuptools&#xff0c;即可解决

K8S测试pod内存和CPU资源不足

只设置requests参数 mysql主从pod启动后监控 读压测之后 同时设置limits和requests&#xff0c;只调低内存值 监控 压力测试 同时设置limits和requests&#xff0c;只调低CPU值 初始状态 开始压测 结论 对于CPU&#xff0c;如果pod中服务使用CPU超过设置的limits&…

(小白教程)MPV.NET 播放器安装和添加插件脚本Bilibili弹幕

MPV.NET安装和添加插件脚本 MPV跨平台播放器&#xff1a;该播放器基于流行的mpv媒体播放器。mpv.net 设计为与 mpv 兼容&#xff0c;几乎所有 mpv 功能都可用&#xff0c;这意味着官方mpv 手册适用于 mpv.net&#xff0c;差异记录在mpv.net 手册中。 主要差异是mpv.net为MPV添加…

Linux 字符设备驱动 之 无法归类的《杂项设备驱动》

学习目标&#xff1a; 了解 杂项设备驱动 和普通字符设备的异同&#xff0c;及杂项设备驱动程序的写法 学习内容&#xff1a; 一、杂项设备驱动的特别之处 杂项设备&#xff08;Miscellaneous Devices&#xff09;是一种通用的设备类型&#xff0c;用于表示那些不适合其他设备…

基于springboot企业微信SCRM管理系统源码带本地搭建教程

系统是前后端分离的架构&#xff0c;前端使用Vue2&#xff0c;后端使用SpringBoot2。 技术框架&#xff1a;SpringBoot2.0.0 Mybatis1.3.2 Shiro swagger-ui jpa lombok Vue2 Mysql5.7 运行环境&#xff1a;jdk8 IntelliJ IDEA maven 宝塔面板 系统与功能介绍 基…

ubuntu 安装haproxy

####安装##### sudo apt update sudo apt install haproxy sudo haproxy -v sudo systemctl status haproxy sudo cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg-org####配置站点##### nano /etc/haproxy/haproxy.cfgfrontend www-httpbind *:5001mode httpdefault_ba…

SAP RFC 的几种类型

SRFC: ARFC: ARFC: TRFC: QRFC: QRFC-QIN Scheduler: *RFC: tables: RSTRFCTA RFC-TEST: Outbound Queue: STOP/RESTART (after STOP) RSTRFCTB RFC TEST: Outbound Queue: Get/Execute LUWs from Local/Remote Syst RSTRFCTC RFC-TEST: Inbound Que…