javaEE初阶——多线程(八)——常见的锁策略 以及 CAS机制

在这里插入图片描述

T04BF

👋专栏: 算法|JAVA|MySQL|C语言

🫵 小比特 大梦想

此篇文章与大家分享分治算法关于多线程进阶的章节——关于常见的锁策略以及CAS机制
如果有不足的或者错误的请您指出!

多线程进阶

1.常见的锁策略

我们需要了解的是,我们使用是锁,在加锁 / 解锁 / 遇到锁冲突的时候,都会怎么做

1.1乐观锁和悲观锁

指的是,在加锁的时候,遇到锁冲突的概率是大 还是 小,如果
如果预测当前出现锁冲突的概率大,那么说明后续要做的工作就会更多,加锁开销的时间就会更大,那么此时就是悲观锁
相反,如果预测当前出现锁冲突的概率小,那么说明后续要做的工作就会少一些,加锁开销的时间就会少一些,那么此时就是乐观锁
我们之前讲到的synchronized,本质上既是乐观锁又是悲观锁
指的是,对于synchronized来说,它是只是"自适应"的,在加锁的过程中,会自动统计出当前出现锁冲突的概率是大还是小
如果大,就会按照悲观锁的方式来执行(做的工作更多),此时往往是要通过内核来完成一些事情的
如果大,就会按照乐观锁的方式来执行(做的工作更少),此时往往是纯用户态的一些操作

1.2重量级锁 和 轻量级锁

一个锁是重量级锁还是轻量级锁,本质上取决于要做的工作的多与少
这样看来,实际上重量级锁和轻量级锁 与 悲观锁和乐观锁 的重叠度是很高的
这是因为,如果加锁过程中要做的事情少,那就是轻量级锁,加锁过程中要做的事情多,那就是重量级锁
而一般锁冲突概率高的时候,需要做的工作就多,低的时候就少
因此,按照我们上面所说,synchronized即使重量级锁又是轻量级锁

1.3自旋锁和挂起等待锁

自旋锁,是轻量级锁的一种典型实现方式
用伪代码来实现自旋锁就是

void lock () {while (true) {if(锁是否被占用) {continue;}获取到锁break;}
}

自旋锁的功能就是,当一个线程尝试获取锁的时候,如果获取锁失败了,那么说明此时的锁正在被占用,出现了锁冲突,就会让当前这个线程进行一段自旋操作,即在循环中不断地尝试获取锁,而不会立即进入阻塞状态。
但是这种方式消耗了大量的系统资源,大部分处于忙等状态,但一旦锁被释放,就能立即获取到锁(拿到锁的速度更快了,但是消耗cpu)

而挂起等待锁是重量级锁的一种典型实现方式,实际上是借助系统的线程调度机制,当一个线程尝试获取锁,并且锁被占用了,出现了锁冲突,就会让当前这个线程挂起等待(阻塞状态),就不去参与调度了
直到锁被释放了,然后系统才去唤醒这个线程,去尝试重新获取锁
此时消耗的时间更长,一旦线程被阻塞了,即使锁释放了,什么时候唤醒,都是不可控的,可能会消耗很长的时间(拿到锁的速度慢了,节省cpu)

在java中,synchronized的轻量级锁部分是基于自旋锁实现的,而重量级锁是基于挂起等待锁实现的

1.4可重入锁和不可重入锁

这一点我们在介绍死锁的文章有提过
在java中,synchronized就是可重入锁

1.5公平锁和非公平锁

公平锁本质上就是严格按照线程的先来后到顺序来获取锁,哪个线程等待的时间长,哪个线程就先拿到锁
而在非公平锁中,谁先获取到锁,是随机的,和线程调度时间就无关了

而java里面的synchronized就是非公平锁,即多个线程尝试获取同一把锁,此时是按照概率均等的方式来获取的

这是由于系统本事线程调度就是随机的,如果要实现公平锁,那么就要引入额外的队列,按照加锁的顺序把这些获取锁的线程入队列,再一个一个的获取

1.6互斥锁和读写锁

synchronized本身就是一个互斥锁,而读写锁则是更加特殊的一种锁
对于互斥锁,本质上就两步操作,加锁和解锁
而读写锁要区分加读锁和加写锁
就是要实现,不同线程之间,读和读不会产生互斥

在日常开发中,有很多场景都是属于"读多 写少",如果使用普通的互斥锁,此时,每次读操作之间,即使不会产生线程安全问题,也会互斥,此时就会比较影响效率

1.7synchronized的自适应过程

按照我们上面所说的几个锁策略,synchronized是"乐观锁和悲观锁",“轻量级锁和重量级锁”,“轻量级锁部分是基于自旋锁实现的,重量级锁部分是基于挂起等待锁实现的”,“不可重入锁”,“非公平锁”,“互斥锁”

synchronized的自适应过程就是
+未加锁的状态 -----(开始执行synchronized) —> 偏向锁 -----遇到锁冲突----> 轻量级锁 ------锁冲突概率进一步提升 ----> 重量级锁

关于偏向锁
指的是,实际上我们使用synchronized进行加锁的时候,一开始不是真正加锁了,而只是做了一个标记,非常轻量,几乎没有开销
此时,如果没有别的线程针对同一个锁对象进行加锁,那么就会一直保持这个状态,直到解锁
但是如果在偏向锁的情况下,发现有别的线程针对同一个锁对象进行加锁,就立马把偏向锁升级为轻量级锁,此时就是真正加锁了,就会产生互斥了

本质上,偏向锁的思想就是"懒"的体现,能不加锁就不加锁,能晚加锁就晚加锁,就能省下很多锁的开销
注意:上述锁的升级过程是不可逆的

1.8锁消除

指的是,如果你的代码里面出现加锁操作,此时编译器就会自动帮你判断,当前这里是不是真的要进行加锁,如果不是,就会自动帮你把锁给优化掉
最典型的就是,当你在单线程里进行加锁操作

1.9锁粗化

本质上与锁的"粒度"有关
在加锁的范围内,包含的代码越多,就认为锁的粒度越粗,反之越细

锁粗化实际上也是一种优化策略,有的时候,需要频繁加锁解锁,此时编译器就会自动把多次细粒度的锁,合并成一次粗粒度的锁

2.CAS机制

所谓CAS,就是compare and swap,即比较和交换
指的是通过 一条cpu指令,就能完成 比较和交换 这样一套操作,即"原子的"

2.1CAS的流程

我们可以将他想象成一个方法

boolean CAS(address,reg1,reg2){if(*address == *reg1){将address里面的值和reg2寄存器里面的值进行交换return true;}return flase;
}

而我们上述模拟的这套操作,是通过一条cpu指令来完成的
实际上上述说的交换,我们通常是用来表示"赋值",因为实际上我们并不关心寄存器里面存的是什么值,而更加关心交换到内存里面的值是多少

因此上述操作也可以近似认为是将寄存器里面的值赋值到内存中

2.2CAS的使用

由于cpu提供了这样的指令,那么操作系统就会对应提供执行这样的指令的API,而JVM又对这样的API进行了封装,那么在java代码中也就直接使用CAS操作了

但是在java里面,CAS相关的类是被封装在unsafe包里面的,使用这里面的东西容易出错,就不鼓励直接使用CAS
在java中,又有一些类是对CAS进行了进一步的封装,典型的就是"原子类"
在这里插入图片描述
举其中的一个例子,我们上面圈出来的AtomicInteger
就相当于针对int进行了封装,就可以保证此处的++,–等操作操作是原子的了
我们在之前写过一个存在线程安全问题的代码
在这里插入图片描述
而当时我们的解决策略就是进行加锁,进行加锁,就会触发阻塞等待,只要代码有了加锁,就基本是与高性能无缘了
实际上这里用CAS会更好
在这里插入图片描述
可以发现,此时就不能简单的对count进行++/–操作了,而正确的操作是:

               count.getAndIncrement();//后置++count.getAndDecrement();//后置--count.decrementAndGet();//前置--count.incrementAndGet();//前置++count.getAndAdd(10);//count += 10

那么由于此时是原子操作,线程就是安全的了
在这里插入图片描述
此处我们的代码中就不涉及任何的加锁操作了

使此时的代码以更高的效率来执行程序

这一套基于CAS,不加锁来实现线程安全代码的方式,也称为"无锁编程"

虽然CAS机制挺好的,但是实际上使用范围没有锁广泛,只能是针对一些特殊场景,使用CAS是更高效的,但是有些场景是不适合使用CAS的

2.3基于CAS实现自旋锁

在java中,synchronized的自旋锁,就是基于CAS实现的
我们通过伪代码来理解自旋锁的实现

public class SpinLock {private Thread owner = null;//表示当前持有锁的线程是谁,如果此时处于未加锁状态,那owner就是nullpublic void lock() {//通过CAS查看当前的锁对象是否已经被获取,如果是,那就要自旋等待//如果不是,那就把owner设置为当前尝试进行加锁的线程,此时cas就返回true,再取反,就是false,循环结束while(!CAS(this.owner,null,Thread.currentThread())) {}}public void unLock() {this.owner = null;}
}

而此处的比较和赋值就是原子的,就可以借助这样的逻辑来进行加锁的实现
ABA问题是属于CAS的一个重要注意事项

2.4CAS的ABA问题

我们前面说过,CAS机制的核心就是"比较 -> 发现相等 -> 交换",即检查当前内存里的值是否被其他线程修改,如果被修改了,就要稍后重试,如果没被修改,接下来就可以修改(不会有安全问题)

但是发现相等的潜台词就是 中间数据没有变化过

但是事实上,在中间可能会有其他线程将这个数从 A - B - A

那么此时就会出现 看起来好像没人修改,实际上已经被改过了的情况

而CAS机制是无法区分当前这个数据是确实没被修改,还是被修改了又改回来

虽然大部分情况下是没有影响的

但是在极端情况下会出现bug

举个例子就是取款场景

假设我们的取款操作是这样类似这样的

在这里插入图片描述

但是有可能发生就是某个人在一次取款操作中多按了一次,导致产生了两个线程来执行上述的扣款操作,那么假设我们的执行流程是:

在这里插入图片描述

那么此时t1线程什么都不执行,什么都不执行是对的!

但是如果此时恰好有第3个人线程给这个人的账户里面转了500,那么就会出问题了

在这里插入图片描述

那么此时t1就又扣款一次,导致取的是500.实际上扣的是1000

上述场景就是典型的ABA问题,实际上是很极端的情况

用户要点击两次 && 恰好在这个时候有人给你转了500 && 恰好转账金额是500

诸多的巧合,哪一环扣不上都不会出现上述的问题

如何解决ABA问题??

核心思路就是,使用账户余额作为判定条件本身就不太合理,因为账户余额本身就属于"能加也能减",就容易出现ABA问题

合理的是,我们可以引入"版本号".约定版本号只能加,不能减,每次操作一遍余额,版本号就+1,通过CAS判定版本号是否与当前版本号相同,如果是就可以进行下一步操作,如果版本号没变过,那么数据就一定没变过

在这里插入图片描述

感谢您的访问!!期待您的关注!!!

在这里插入图片描述

T04BF

🫵 小比特 大梦想

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

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

相关文章

1、opencv介绍与开发环境搭建

1、opencv介绍 OpenCV 是 Intel 开源计算机视觉库,是一个跨平台的开源计算机视觉和机器学习软件库。它由一系列 C 函数和少量 C 类构成,可用于开发实时的图像处理、计算机视觉以及模式识别程序。 该库有 2500 多种优化算法,其中包括一套全面…

联合和枚举

1、联合 联合(union)是一种数据类型,它能在用一个内存空间中存储不同的数据类型(但不是同时存储)。其典型的用法是,设计一种表以存储既无规律、事先也不知道顺序的混合类型。 使用联合类型的话&#xff0c…

国际收支平衡表的分析要点有哪些

国际收支统计是国民经济综合平衡必不可少的一个环节,国际收支的顺差或逆差可以弥补国内投资和消费与国内生产总值间的缺口,国际收支状况可影响国内信贷平衡,对金融调控至关重要。良好的国际收支统计信息是制定正确的对外经济政策的基础。 一…

解读币安Megadrop:如何参加第一期BounceBit活动?

币安推出新的代币发行平台 Megadrop,第一期为 BounceBit。 跟 launchpool 相比, 主要不同是 1)锁仓 bnb 有收益的倍数加成 2)做任务有收益加成。 我认为核心目的有两个: 1)更多收益给 BNB 长期持有者&am…

NovaMSS音乐源分离v1.3.3社区版

软件介绍 NovaMSS 基于最新 AI 模型优化的音乐源分离工具。它能够轻松地批量提取伴奏、人声、贝斯、鼓点等音轨,并且支持 GPU 加速,以提高处理速度和效率。社区版完全免费,简单易用,上传文件,点击处理,查看…

K8s七层代理Ingress Controller详解

🐇明明跟你说过:个人主页 🏅个人专栏:《Kubernetes航线图:从船长到K8s掌舵者》 🏅 🔖行路有良友,便是天堂🔖 目录 一、前言 1、k8s简介 2、Ingress Controller简介…

vlan的学习笔记2(vlan间通信)

1.使用路由器的物理接口 原理:在二层交换机上配置VLAN,每个VLAN单独使用一个交换机接口与路由器互联。路由器使用两个物理接口,分别作为VLAN 10及VLAN 20内PC的默认网关,使用路由器的物理接口实现VLAN之间的通信。 实验1&#x…

每一个优秀的Java程序员都应该掌握的90条编程法则

经典Jolt 获奖作品Effective Java的第3版,从Java 5 到Java 9的种种特性,Java 开发人员不可缺少的一本参考书:Effective Java中文版(原书第3版),2024全新译本,豆瓣原版评分9.6,中文版9.8。 作者约书亚.布洛克…

vue-manage-system 版本更新,让开发更加简单

vue-manage-system 近期进行了一次版本升级,主要是支持了更多功能、升级依赖版本和优化样式,并且上线了官方文档网站,大部分功能都有文档或者使用示例,更加适合新手上手开发,只需要根据实际业务简单修改,就…

跟着Datawhale重学数据结构与算法(3)---排序算法

开源链接:【 教程地址 】【电子网站】 【写博客的目的是记录自己学习过程,方便自己复盘,专业课复习】 数组排序: #mermaid-svg-F3iLcKsVv8gcmqqC {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16p…

Centos 5 的yum源

背景 有使用较老的Centos 5 系统内部安装软件无法正常报错,是由于系统叫老yum源存在问题 处理方法 更换下述yum源,可以将其他repo源文件备份移动到其他目录,添加下述源后重新测试 [C5.11-base] nameCentOS-5.11 baseurlhttp://vault.c…

A Neural Span-Based Continual Named Entity Recognition Model

《A Neural Span-Based Continual Named Entity Recognition Model》------------AAAI’23 论文链接:https://arxiv.org/abs/2302.12200 代码:https://github.com/Qznan/SpanKL 当前问题: 1.现有的NER模型在适应新的实体类型时往往表现不佳…