一文带你了解乐观锁和悲观锁的本质区别!

文章目录

  • 悲观锁是什么?
  • 乐观锁是什么?
  • 如何实现乐观锁?
    • 什么是CAS
    • 应用
    • 局限性
    • ABA问题是什么?

悲观锁是什么?

悲观锁它总是假设最坏的情况,它会认为共享资源在每次被访问的时候就会出现线程安全问题,所以每次在获取资源的时候都会上锁,以避免线程安全问题发生

也就是说,共享资源每次只给一个线程使用,而其他的线程则会阻塞住,当占据锁的线程用完后才会把共享资源释放掉,让给其它线程来进行竞争。

这样就会导致在高并发的场景下容易造成死锁、以及线程阻塞等,增加系统的开销。

乐观锁是什么?

乐观锁总是假设最好的情况,它认为共享资源每次被访问的时不会出现线程问题,所以也就不用加锁去保证线程安全,因此线程可以不停地执行,只有当提交修改的时候去验证对应的共享资源是否被其它线程修改。

高并发的场景下,乐观锁不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。
但是,如果写操作的冲突频繁发生,会频繁失败和重试,这样同样会非常影响性能。

如何实现乐观锁?

什么是CAS

CAS是Compare-And-Swap(比较并交换)的缩写,是一种轻量级的同步机制,主要用于实现多线程环境下的无锁算法和数据结构,保证了并发安全性。它可以在不使用锁的情况下,对共享数据进行线程安全的操作。

它就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。CAS 操作是一个原子操作,它在执行期间不会被其他线程中断。因此,它能够提供一种乐观并发控制机制,避免了传统锁机制的开销和可能的线程阻塞。

它的其实主要就是两个步骤:冲突检测以及数据更新

通常包含三个参数:内存位置(或称为变量)、期望值新值。它的执行步骤如下:
  1. 读取内存位置的当前值。
  2. 检查当前值是否与期望值相等。如果相等,则进行步骤4;如果不相等,则说明其他线程已经修改了该值,操作失败。
  3. 如果当前值与期望值相等,则将新值写入内存位置。
  4. 返回操作是否成功的标志。

class AccountSafe implements Account {private AtomicInteger balance; // 原子整数类型 public AccountSafe(Integer balance) {this.balance = new AtomicInteger(balance);}@Overridepublic Integer getBalance() {return balance.get();}@Overridepublic void withdraw(Integer amount) {while (true) {// 没同步到主存 因为是局部变量,只在线程的工作内存之中int prev = balance.get(); // 获取余额最新值int next = prev - amount; // 修改后的余额// 真正修改if (balance.compareAndSet(prev, next)) { // 成功为true;失败false,继续循环 break;}}}
}

我们再来仔细看一下withdraw方法

public void withdraw(Integer amount) {// 需要不断尝试,直到成功为止while (true) {// 比如拿到了旧值 1000int prev = balance.get();// 在这个基础上 1000-10 = 990int next = prev - amount;/*compareAndSet 正是做这个检查,在 set 前,先比较 prev 与 当前值!!!当不一致时,next 作废,返回 false 表示失败比如,别的线程已经做了减法,当前值已经被减成了990那么本线程的这次 990 就作废了,进入 while 下次循环重试直到一致,以 next 设置为新值,返回 true 表示成功*/if (balance.compareAndSet(prev, next)) {break;}}
}

在并发环境中,多个线程可以同时执行CAS操作来更新同一个内存位置的值。如果多个线程同时执行CAS操作,只有一个线程的CAS操作会成功,其他线程的操作将失败。在失败的情况下,可以选择重试CAS操作。

应用

  1. JVM创建对象的过程中分配内存【堆中 因为这个是共享 所以要保证安全】
  2. syn轻量级锁的时候,JVM尝试使用CAS操作,将对象头的Mark Word更新为指向锁记录的指针。
  3. ReentrantLock中的非公平锁,也使用CAS来管理锁的状态。比如,尝试获取锁时会使用CAS来检查并更新锁的状态。
  4. 并发集合:如ConcurrentHashMap等,并发集合的实现中也大量使用了CAS操作,以实现高效的线程安全访问。
  5. 原子类:如AtomicIntegerAtomicLongAtomicReference等,这些类提供了一组原子操作,允许你在单个操作中安全地读取、写入和更新变量。这些操作背后就是通过CAS来实现的。

局限性

  1. 只能保证对单个共享变量的操作是原子性的,无法保证对多行代码实现原子性
  2. 高并发场景下,竞争激烈,CAS 失败重试会频繁发生,自旋时间过长,而线程又不阻塞,抢占 CPU 资源,导致 CPU 使用率飙升,反而影响了性能
    a. 指定 CAS 一共循环多少次,如果超过这个次数,直接失败或将线程挂起(参考 synchronized 中的自旋锁) .
    b. 可以通过分段的思想减少竞争,使用原子累加器 LongAdder,当有竞争时设置多个累加单元,最后将结果汇总
  3. ABA问题

ABA问题是什么?

先看例子:

假设你在银行的查看账户余额。第一次查看时,余额显示为100元(状态A)。然后打算取出50元,但在操作之前,出于确认目的,再次检查余额,发现还是100元,似乎没有变化(仍然是状态A)。

但实际情况可能是,在两次查看之间,有人往你的账户存入了50元(状态变为B:150元),然后又立即取出了50元(状态再次回到A:100元)。尽管最终余额回到了初始查看的数值,但实际上账户经历了存取的变化(A->B->A)。

在并发编程的上下文中,这就是“ABA问题”。当你CAS操作来确保数据一致性时,如果仅比较前后值是否相同(都是A),就可能会忽略掉中间发生的改变(B状态),误以为数据从未被改动过,从而可能导致逻辑错误或数据不一致性!

如何解决?

解决ABA问题的一种常见方法是引入版本号或者时间戳,每次修改变量时不仅更新其值,还增加版本号或时间戳。这样,即便值回到了最初的状态,通过检查版本号或时间戳的不同,也可以察觉到变量曾经被修改过。

AtomicStampedReference(维护版本号)
AtomicStampedReference通过捆绑一个引用及其关联的stamp(印记,可以视为版本号或时间戳)来工作,以此增强传统的比较并交换(CAS)操作。

它允许线程在执行 CAS 操作时,不仅检查引用是否发生了变化,还要检查时间戳是否发生了变化。这样,即使一个变量的值被修改后又改回原值,由于时间戳的存在,线程仍然可以检测到这中间的变化。

public class AtomicStampedReferenceDemo {private static final Logger log = LoggerFactory.getLogger(AtomicStampedReferenceDemo.class);static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);public static void main(String[] args) throws InterruptedException {log.debug("main start...");// 获取值 AString prev = ref.getReference();// 获取版本号int stamp = ref.getStamp();log.debug("版本 {}", stamp);// 如果中间有其它线程干扰,发生了 ABA 现象other();TimeUnit.MILLISECONDS.sleep(1); // 使用TimeUnit使代码更具可读性// 尝试改为 Clog.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));}private static void other() {new Thread(() -> { // 更新如果成功,版本号加1log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",ref.getStamp(), ref.getStamp() + 1));log.debug("更新版本为 {}", ref.getStamp());}, "t1").start();TimeUnit.MILLISECONDS.sleep(500); // 确保t1先启动new Thread(() -> {log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",ref.getStamp(), ref.getStamp() + 1));log.debug("更新版本为 {}", ref.getStamp());}, "t2").start();}private static void sleep(long millis) {try {Thread.sleep(millis);} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException(e);}}
}

AtomicMarkableReference(仅维护是否修改过)
AtomicStampedReference不同,它通过一个布尔标记(mark),来简单指示引用的对象是否曾被修改过。
这个类在执行CAS时,不仅关注引用本身的比较,还会检查这个伴随的标记状态。即,哪怕对象的值在一段时间内经历了A->B->A,由于标记的存在,线程也能够感知到该对象曾经发生过变化。
在这里插入图片描述

// GarbageBag类定义
class GarbageBag {private String desc;public GarbageBag(String desc) {this.desc = desc;}public void setDesc(String desc) {this.desc = desc;}@Overridepublic String toString() {return "GarbageBag{" +"desc='" + desc + '\'' +'}';}
}public class TestABAAtomicMarkableReference {private static final Logger log = LoggerFactory.getLogger(TestABAAtomicMarkableReference.class);public static void main(String[] args) throws InterruptedException {GarbageBag bag = new GarbageBag("装满了垃圾");// 参数2 mark 可以看作一个标记,表示垃圾袋是否已满AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);log.debug("主线程 start...");GarbageBag prev = ref.getReference();log.debug(prev.toString());new Thread(() -> {log.debug("打扫卫生的线程 start...");bag.setDesc("空垃圾袋"); // 假设这里清理了垃圾袋// 尝试将标记从true改为false,表示垃圾袋已清空while (!ref.compareAndSet(bag, bag, true, false)) {}log.debug(bag.toString());}).start();TimeUnit.SECONDS.sleep(1); // 等待打扫卫生的线程执行log.debug("主线程想换一只新垃圾袋?");boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);log.debug("换了么?" + success);log.debug(ref.getReference().toString());}
}

其他文章

从底层源码剖析AQS的来龙去脉!(通俗易懂)

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

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

相关文章

JVM调优(一)——JVM调优诊断工具详解

最近项目要生产上线&#xff0c;正在做压测性能测试&#xff0c;开始进行一些性能瓶颈分析&#xff0c;记得上一次做性能分析优化&#xff0c;还是国网项目&#xff0c;针对Kafka,Canal,ES,服务&#xff0c;数据库等一系列的排查分析&#xff0c;后面打算补一下总结内容&#x…

安全和加密常识(6)Base64编码方式

文章目录 什么是 Base64编码原理编解码示例应用什么是 Base64 Base64 是一种用于将二进制数据编码为仅包含64种ASCII字符的文本格式的编码方法,注意,它不是加密算法。它设计的目的主要是使二进制数据能够通过只支持文本的传输层(如电子邮件)进行传输。Base64常用于在需要处…

Windows系统下文件夹权限详解

文章目录 问题描述文件夹属性 问题描述 今天在Win10系统下&#xff0c;实现文件夹设置权限&#xff0c;具体的方案的涉及到我们公司内部的一款加密软件&#xff0c;不太方便透漏&#xff0c;借此机会&#xff0c;我也重新的回顾下windows系统下的文件夹权限 文件夹属性 打开…

vue3+Ts封装axios网络请求

1.安装axios npm i axios 在package.json中检查axios是否安装成功 "dependencies": {"axios": "^1.7.2","vue": "^3.4.29","vue-router": "^4.4.0"}, 2.新建文件 新建文件utils/request.ts import…

js 接收回调函数 转换为promise

下面是一个示例代码&#xff0c;展示如何编写一个接收回调函数并将其转换为 Promise 的 JavaScript 函数&#xff1a; // 定义一个接收回调函数并转换为 Promise 的函数 function convertCallbackToPromise(callbackFunction) {// 返回一个新的 Promise 对象return new Promis…

Java 并发编程常见问题

1、线程状态它们之间是如何扭转的&#xff1f; 1、谈谈对于多线程的理解&#xff1f; 1、对于多核CPU&#xff0c;多线程可以提升CPU的利用率&#xff1b; 2、对于多IO操作的程序&#xff0c;多线程可以提升系统的整体性能及吞吐量&#xff1b; 3、使用多线程在一些场景下可…

鸿蒙开发设备管理:【@ohos.multimodalInput.inputDevice (输入设备)】

输入设备 输入设备管理模块&#xff0c;用于监听输入设备连接、断开和变化&#xff0c;并查看输入设备相关信息。比如监听鼠标插拔&#xff0c;并获取鼠标的id、name和指针移动速度等信息。 说明&#xff1a; 本模块首批接口从API version 8开始支持。后续版本的新增接口&…

【算法专题--栈】用队列实现栈 -- 高频面试题(图文详解,小白一看就懂!!)

目录 一、前言 二、题目描述 三、解题方法 ⭐两个队列实现栈 &#x1f95d;解题思路 &#x1f34d;案例图解 ⭐用一个队列实现栈 &#x1f347;解题思路 &#x1f34d;案例图解 四、总结与提炼 五、共勉 一、前言 用队列实现栈 这道题&#xff0c;可以说是--栈…

WAF 相关的术语解释

QPS 每秒查询率&#xff08;Query Per Second QPS&#xff09; 是对一个特定的查询服务器&#xff0c;在规定时间内所处理流量多少的衡量标准&#xff0c;在因特网上&#xff0c;作为域名系统服务器的机器性能经常用每秒查询率来衡量&#xff0c;对应 fetches/sec&#xff08;…

论文写作笔记9 word论文排版常见问题

1.公式编写 word公式编写麻烦, 我推荐latex编写转word或识图. 识图可以使用软件Mathpix Snipping Tool, latex转word见下方链接. word中输入的Latex代码&#xff0c;按 Alt 将所选字母变成公式&#xff0c; 然后按 Ctrl 将 Latex 代码转换成 Word 公式 MAML在线互转 …

002-基于Sklearn的机器学习入门:回归分析(上)

本节及后续章节将介绍机器学习中的几种经典回归算法&#xff0c;所选方法都在Sklearn库中聚类模块有具体实现。本节为上篇&#xff0c;将介绍基础的线性回归方法&#xff0c;包括线性回归、逻辑回归、多项式回归和岭回归等。 2.1 回归分析概述 回归&#xff08;Regression&…

【深度学习】Speech2Action: Cross-modal Supervision for Action Recognition

文章目录 Speech2Action: 基于跨模态监督的动作识别摘要1. 引言2. 相关工作将剧本与电影对齐动作识别的监督 3. Speech2Action模型3.1 IMSDb 数据集剧本解析动词挖掘舞台指令基于BERT的语音分类器实现细节结果 4. 动作识别视频挖掘4.1 未标注的数据4.2 获取弱标签4.2.1 使用Spe…

《昇思25天学习打卡营第14天 | 昇思MindSpore基于MindNLP+MusicGen生成自己的个性化音乐》

14天 本节学了基于MindNLPMusicGen生成自己的个性化音乐。 MusicGen是来自Meta AI的Jade Copet等人提出的基于单个语言模型的音乐生成模型&#xff0c;能够根据文本描述或音频提示生成高质量的音乐样本。 MusicGen模型基于Transformer结构&#xff0c;可以分解为三个不同的阶段…

springboot笔记示例六:fastjson2集成

springboot笔记示例六&#xff1a;fastjson2集成 本文md下载 https://download.csdn.net/download/a254939392/89491102本文md文档下载地址 #springboot json官方说明 https://docs.spring.io/spring-boot/docs/2.1.6.RELEASE/reference/html/boot-features-json.htmlsprin…

HP UX服务器监控指标解读(Snmp)

随着企业信息化建设的不断深入&#xff0c;服务器的稳定运行成为了保障业务连续性的关键。HP UX作为一款高性能的Unix服务器操作系统&#xff0c;在各类企业级应用中发挥着重要作用。为了确保HP UX服务器的稳定运行&#xff0c;对其进行全面而细致的监控至关重要。本文将针对监…

⭐ UI自动化工具轻松实现微信消息提醒 ⚡

&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f; 演示效果 &#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f; &#x1f605;&#x1f605;&#x1f605;&#x1f605;&#x1f605;&#x1f605; Python安装…

2023HW部分笔试题

题目来源&#xff1a;卡码网 136. 字符串处理器 问题描述 时间限制&#xff1a;1.000S 空间限制&#xff1a;256MB 题目描述 产品代码需要设计一个带游标的字符串处理器&#xff0c;它需要实现以下功能: 插入&#xff1a;在游标所在处添加文本&#xff0c;其对应操作为 i…

AI时代,你的工作会被AI替代吗?

AI在不同领域的应用和发展速度是不同的。在智商方面&#xff0c;尤其是在逻辑推理、数据分析和模式识别等领域&#xff0c;AI已经取得了显著的进展。例如&#xff0c;在国际象棋、围棋等策略游戏中&#xff0c;AI已经能够击败顶尖的人类选手。在科学研究、医学诊断、股市分析等…

一分钟彻底掌握Java枚举

在Java编程语言中&#xff0c;枚举&#xff08;Enum&#xff09;是一种特殊的类&#xff0c;它包含了一组固定的常量。枚举常用于表示固定数量的常量值&#xff0c;例如一周的七天、四个基本方向&#xff08;东、南、西、北&#xff09;等。 0.浅显理解 枚举就像是一个特殊的…

STM32 SWD烧写

最小电路 stm32f103x 内部已经集成了振荡电路&#xff0c;可以省略&#xff1b;rst引脚电路&#xff0c;可以省略&#xff0c;boot0,boot1不需要设置 正常烧录 -------------------------------------------------------------------STM32CubeProgrammer v2.9.0 …