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

文章目录

  • 悲观锁是什么?
  • 乐观锁是什么?
  • 如何实现乐观锁?
    • 什么是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.rhkb.cn/news/364988.html

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

相关文章

AI陪伴产品的情感设计:从孤独感到恋爱感评分:9/10

本文主要阐述三个话题&#xff1a; 1. 市面上有哪些AI陪伴产品&#xff1f; 2. 我们团队要怎么做&#xff1f; 3. 为什么要做&#xff1f; 市面上有哪些陪伴类产品&#xff1f; Role-play&#xff08;角色扮演&#xff09; 在当前市场上&#xff0c;有不少以角色扮演为核心的…

qt文件如何打包成一个独立的exe文件

QT官方给我们安装好了打包软件&#xff0c;就在你QT安装的位置 把这个在cmd打开C:\Qt\6.7.1\mingw_64\bin\windeployqt6.exe&#xff08;或复制地址&#xff09; 然后把要打包项目的exe复制到新的空文件夹&#xff0c;再复制他的地址 按回车后生成新文件 再下载打包软件&#…

Coursera耶鲁大学金融课程:Financial Markets 笔记Week 03

Financial Markets 本文是学习 https://www.coursera.org/learn/financial-markets-global这门课的学习笔记 这门课的老师是耶鲁大学的Robert Shiller https://en.wikipedia.org/wiki/Robert_J._Shiller Robert James Shiller (born March 29, 1946)[4] is an American econom…

【地理库 Turf.js】

非常全面的地理库 &#xff0c; 这里枚举一些比较常用&#xff0c;重点的功能&#xff0c; 重点功能 提供地理相关的类&#xff1a;包括点&#xff0c;线&#xff0c;面等类。 测量功能&#xff1a;点到线段的距离&#xff0c;点和线的关系等。 判断功能&#xff1a; 点是否在…

西点领导力:卓越是怎样练成的

今天刚看了一个美国西点军校第50任校长&#xff1a;罗伯克卡斯伦的《为什么跟西点军校学领导力培养》这个演讲。从中受益良多&#xff0c;于是我就去了解了一下这位校长以及西点军校。 西点军校 西点军校&#xff08;United States Military Academy, USMA&#xff09;&#…

C++感受12-Hello Object 派生版

不变的功能&#xff0c;希望直接复用原有代码&#xff1b;变化的功能&#xff0c;希望在分开的代码里实现。 派生的基本概念和目的如何定义派生类以及创建派生对象派生对象的生死过程 0. 课堂视频 ff14-HelloObject-派生版 1. 派生的基本概念与目的 编程&#xff0c;或者说软…

Linux线程同步【拿命推荐版】

目录 &#x1f6a9;引言 &#x1f6a9;听故事&#xff0c;引概念 &#x1f6a9;生产者消费者模型 &#x1f680;再次理解生产消费模型 &#x1f680;挖掘特点 &#x1f6a9;条件变量 &#x1f680;条件变量常用接口 &#x1f680;条件变量的原理 &#x1f6a9;引言 上一篇…

解锁横向招聘:创新您的人才搜索

技能差距仍然是面试官面临的问题之一。在这些空缺职位中&#xff0c;很难找到合适的技能候选人&#xff0c;特别是高级职位或以上职位。另一方面&#xff0c;申请人也发现很难找到一份适合自己的工作&#xff0c;因为他们抱怨工作要求太窄或太具体。在具有挑战性的职位招聘环境…

魔行观察-烤匠麻辣烤鱼-开关店监测-时间段:2011年1月 至 2024年6月

今日监测对象&#xff1a;烤匠麻辣烤鱼&#xff0c;监测时间段&#xff1a;2011年1月 至 2024年6月 本文用到数据源获取地址 魔行观察http://www.wmomo.com/ 品牌介绍&#xff1a; 2013年&#xff0c;第一家烤匠在成都蓝色加勒比广场开业&#xff0c;随后几年成都国金中心店…

Qt开发 | Qt界面布局 | 水平布局 | 竖直布局 | 栅格布局 | 分裂器布局 | setLayout使用 | 添加右键菜单 | 布局切换与布局删除重构

文章目录 一、Qt界面布局二、Qt水平布局--QHBoxLayout三、Qt竖直布局四、Qt栅格布局五、分裂器布局代码实现六、setLayout使用说明七、布局切换与布局删除重构1.如何添加右键菜单2.布局切换与布局删除重构 一、Qt界面布局 Qt的界面布局类型可分为如下几种 水平布局&#xff08;…

木各力“GERRI”被“GREE”格力无效宣告成功

近日“GERRI”被“GREE”格力无效宣告成功&#xff0c;“GERRI”和“GREE”近似不&#xff0c;如果很近似当初就不会通过初审和下商标注册证&#xff0c;但是如果涉及知名商标和驰名商标&#xff0c;人家就可以异议和无效。 “GERRI”在被无效宣告时&#xff0c;引用了6个相关的…

【python刷题】【深基5.例5】旗鼓相当的对手

题目描述 算法思路 用二维数组data存放成绩数据。双重循环遍历所有的组合&#xff0c;因为自己不能和自己比&#xff0c;所以要注意内层遍历的起始位置。新建一个数组用来得出各个科目的分差&#xff0c;便于代码的书写。由于分差计算出来会出现负数&#xff0c;所以比较的时候…

探索MySQL核心技术:理解索引和主键的关系

在数据密集型应用中&#xff0c;数据库的性能往往是决定一个应用成败的重要因素之一。其中&#xff0c;MySQL作为一种开源关系型数据库管理系统&#xff0c;以其卓越的性能和丰富的功能被广泛应用。而在MySQL数据库优化的众多技巧中&#xff0c;索引和主键扮演着极其重要的角色…

Windows怎么实现虚拟IP

在做高可用架构时&#xff0c;往往需要用到虚拟IP&#xff0c;在linux上面有keepalived来实现虚拟ip的设置。在windows上面该怎么弄&#xff0c;keepalived好像也没有windows版本&#xff0c;我推荐一款浮动IP软件PanguVip&#xff0c;它可以实现windows上面虚拟ip的漂移。设置…

WordPress中文网址导航栏主题风格模版HaoWa

模板介绍 WordPress响应式网站中文网址导航栏主题风格模版HaoWa1.3.1源码 HaoWA主题风格除行为主体导航栏目录外&#xff0c;对主题风格需要的小控制模块都开展了敞开式的HTML在线编辑器方式的作用配备&#xff0c;另外预埋出默认设置的编码构造&#xff0c;便捷大伙儿在目前…

常用字符串方法<python>

导言 在python中内置了许多的字符串方法&#xff0c;使用字符串方法可以方便快捷解决很多问题&#xff0c;所以本文将要介绍一些常用的字符串方法。 目录 导言 string.center(width[,fillchar]) string.capitalize() string.count(sub[,start[,end]]) string.join(iterabl…

Qt学习之ui创建串口助手

一、串口简介 二、Qt编写串口助手 1、创建Qt工程 选择MinGW 64-bit 点击下一步完成&#xff0c;工程创建完成。 使用串口模块&#xff0c;需要在工程文件.pro中添加以下代码&#xff0c;不添加的话&#xff0c;会报错。 或者在core gui 后输入 serialport 也可以 2、配置UI…

【Unity】RPG2D龙城纷争(六)关卡编辑器之角色编辑

更新日期&#xff1a;2024年6月26日。 项目源码&#xff1a;第五章发布&#xff08;正式开始游戏逻辑的章节&#xff09; 索引 简介一、角色编辑模式1.将字段限制为只读2.创建角色&#xff08;刷角色&#xff09;3.预览所有角色4.编辑选中角色属性5.移动角色位置6.移除角色 简介…

vue中【事件修饰符号】详解

在Vue中&#xff0c;事件修饰符是一种特殊的后缀&#xff0c;用于修改事件触发时的默认行为。以下是Vue中常见的事件修饰符的详细解释&#xff1a; .stop 调用event.stopPropagation()&#xff0c;阻止事件冒泡。当你在嵌套元素中都有相同的事件监听器&#xff08;如click事件…

【Linux系统】进程替换 自主实现shell(简易版)

1.先看代码 && 现象 我们用exec*函数执行新的程序&#xff0c; exec*系列的函数&#xff0c;执行完毕后&#xff0c;后续的代码不见了&#xff0c;因为被替换了。 execl的返回值可以不关心了&#xff0c;只要替换成功&#xff0c;就不会向后继续运行&#xff0c;只要…