【JavaEE】多线程 -- CAS机制(比较并交换)

目录

  • CAS是什么
  • CAS的应用
    • 实现原子类
    • 实现自旋锁
  • ABA问题
    • ABA问题概述
    • ABA问题引起的BUG
    • 解决方案

CAS是什么

  • CAS (compare and swap) 比较并交换,CAS 是物理层次支持程序的原子操作。说起原子性,这就设计到线程安全问题,在代码的层面为了解决多线程并发处理同一共享资源造成的线程安全问题,我们常常会使用 synchronized 修饰代码块,变量等,将程序背后的指令封装成原子性(事务的最小单位,不可再分),当一个线程执行 synchronized 修饰的代码块时获取指定对象的对象锁(一个对象只有一把锁),其他并发处理同一代码块的线程因无法获取对象锁就会进入阻塞等待对象锁释放,然后继续竞争对象锁,此时 synchronized 修饰的代码块就具有原子性,具有互斥性,不可抢占性。
  • CAS 是CPU 物理层次支持的原子操作(一条指令).

读取内存数据(V),预期原值(A)和新值(B)。
如果内存位置的值与预期原值相等,就将新值(B)更新到内存中,替换掉原内存数据,
如果内存位置的值与预期原值不相等,处理器不会做任何操作,
CAS 对数据操作后会返回一个 boolean 值判断是否成功。

  • 以上指令集合,可以视为CPU 物理层次支持的一条指令,同样可以解决多线程并发处理同一共享资源造成的线程安全问题。

CAS 使用Java伪代码理解含义:

// address 原内存值
// expectValue 旧的预期值
// swapValue 需要修改的新值
// & 代表取地址,这里主要是理解这层含义,java 语法不支持
boolean CAS(address, expectValue, swapValue) {if (&address == expectedValue) {&address = swapValue;return true;}return false;
}

在这里插入图片描述

  • 这么表示, 那么我们最终关心的是内存中的值, 至于寄存器的值一般都不要了

CAS的应用

  • 操作系统会对 CPU 指令进行封装,JVM 又会对操作系统提供的 API 再进行一层封装,由于 CAS 本身就是 CPU 指令,所以在 Java 中也有关于 CAS 的 API,关于 CAS 的 API 放在了 unsafe 类里,Java 的标准库中又对 CAS 进行了进一步的封装,并且提供了一些工具类,可以让我们直接使用。

实现原子类

  • 原子类,就是 Java 标准库对 CAS 进行进一步封装后提供的一种工具类,如下图所示:
    在这里插入图片描述
  • 在原子类中可以看到,它对 Integer 和 Long 进行了封装,此时针对这样的对象再进行多线程修改,就是线程安全的了,不知道大家还记不记得前面介绍线程安全时的一个代码示例,就是利用两个线程分别对同一个变量分别进行自增 50000 次,当时用这个代码示例来演示了线程不安全的效果,后来我们通过加锁的方式解决了这样的问题,下面我就来使用原子类来写一个用两个线程对同一变量分别进行 50000 次自增的代码,来看看此时会不会出现线程安全问题,代码及运行结果如下所示:
public class demo36 {private static AtomicInteger count = new AtomicInteger();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for(int i = 0; i < 50000; i++) {count.getAndIncrement(); //前缀自增//count.incrementAndGet(); //后缀自增}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {// count++;count.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

在这里插入图片描述

  •  如上图所示,使用原子类不会出现之前的问题,这是因为之前使用的 result++ 是三个指令,在多线程中的三个指令会穿插执行,所以会引起线程不安全,此处的 getAndIncrement 对变量的修改是一个 CAS 指令,天生就是原子的,就不会出现穿插执行这种问题了,并且这个代码不涉及加锁,不需要阻塞等待,更高效。
    
  • 那么上面的这段代码是如何做到把自增操作变成原子的呢?我们可以进入源码来尝试找寻一下答案,如下图所示:
    在这里插入图片描述
  • 层层点入到了native修饰的代码, 表示是C++写的代码, 我们无法直接能看见. 这里我们用伪代码来理解一下逻辑
// 下面代码是一段伪代码(不能编译执行,只是用来表示逻辑)
class AtomicInteger {// value 表示我们要进行自增操作的变量private int value;public int getAndIncrement() {// oldValue 表示放到寄存器中的值int oldValue = value;// CAS 进行比较交换while (CAS(value, oldValue, oldValue + 1) != true) {// value 与 oldValue 的值不相等,说明在此期间有其他线程修改了 value 的值// 更新 oldValue 的值与 value 相等,再次进行循环oldValue = value;}// 当 value 与 oldValue 相等,就将 oldValue+1 的值赋值给 value// 以此来实现 value 的自增操作return Value;}
}

在这里插入图片描述

  • 可以看到我们判断和赋值都是一条指令, 这里线程的随机调度并不能把判断和赋值分开. 这就保证了我们这里比较和交换是原子的. 和我们不加锁比较和交换指令被线程调度插队不一样, 也就保证了我们线程安全.
  • 可以看到我们使用CAS后这个线程是允许t2线程指令插队在t1线程指令前面的, 个时候他插队了那么肯定内存的值和寄存器的值一定不一样这, 那么我们就需要重新加载寄存器的值到内存中让寄存器和内存中的值保证一样. 这就和我们加锁不让t2线程插队, 执行的结果一样了.
  • 为了确保当前读取到的值是内存中最新值付出的代价就是“自旋”,如果不是最新值就不断的循环判断,直到是最新值后再进行修改,此时就会消耗更多的 CPU 资源。

实现自旋锁

在这里插入图片描述

  • 自旋锁一般用于锁竞争不激烈的情况下,在上述代码中,当 owner 不为 null 的时候,循环就会一直执行,通过这样的“忙等”来完成等待的效果,此处自旋式的等没有放弃 CPU,不会参与到调度中,省去了调度开销,但是会消耗更多 CPU 的资源。

ABA问题

ABA问题概述

在这里插入图片描述

  • 图中的场景就是t1线程想把num值改成100, 这个时候执行过程如下
  1. 先读取 num 的值,记录到 oldNum 变量中;
  2. 使用 CAS 判断当前 num 的值是否为 0,如果为 0 就修改成 100。
  • 但是在这个时候, t1线程修改到num之前, t2线程优先被调度了, t2线程把num修改到100后, 又修改为0 .这个时候t1线程希望改的num是没有变过的, 结果t2改变了又改回去了. t1线程就无法判断到底这个num值有没有被改动过.这种问题就好比我们买了一款新手机,无法分辨这是由别人使用过重新翻新的还是他原本就是新的。

ABA问题引起的BUG

在这里插入图片描述

解决方案

  • 我们从上面的例子中看见, ABA问题出现的核心原因就是另一个线程对同一个变量进行修改, 进行改动(如加操作), 然后又改回去(如减操作), 那么当前线程就并不知道他是进行了对这个变量进行改动(已经执行了减操作)的. 所以再去进行减操作.
  • 为了解决这个问题. 我们约定不允许进行又进行加操作, 又进行减操作.这个时候另外一个线程对一个变量进行改动(如加操作), 就不可能还原原来变量的值(如减操作)对于本身就必须双向变化的数据,可以引入一个版本号,版本号这个数字只能增加不能减少,此时就可以根据版本号来判断当前数字是不是第一次被修改。
  • 这个时候我们再进行上面的扣款操作

t1线程获取到当前余额是1000, 版本号为1, 期望扣款500. 也就是把余额修改为500. 这个时候调度到t2线程
t2线程获取当前余额是1000, 版本号为2, 余额修改为500(扣款成功). 调度到t3线程
t3线程获取当前余额是500, 版本号为3, 想对余额里面转账500. 修改余额为1000. 调度到t1
t1线程当前余额为1000, 与之前获取的余额相同. 但是当前版本号为3, 与之前的版本号1不同. 这个时候期望扣款500就失败了.

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

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

相关文章

The United Nations Is Already Dead

The United Nations Is Already Dead When children in Gaza rummage through rubble for food, when UN-run schools are reduced to dust, when the Security Council cannot even pass the mildest ceasefire resolution—blocked by a single veto— we must confront a br…

Kubernetes v1.34 前瞻:资源管理、安全与可观测性的全面进化

预计正式发布&#xff1a;2025年8月底 | 分类&#xff1a;Kubernetes 随着2025年8月底的临近&#xff0c;Kubernetes社区正紧锣密鼓地准备下一个重要版本——v1.34的发布。本次更新并非简单的功能叠加&#xff0c;而是在资源管理、安全身份、可观测性和工作负载控制等核心领域的…

用 Bright Data MCP Server 构建实时数据驱动的 AI 情报系统:从市场调研到技术追踪的自动化实战

前言 本文通过两个真实场景&#xff08;云服务商对比与 AIGC 技术追踪&#xff09;&#xff0c;展示了如何使用 Bright Data MCP Server 与 Lingma IDE 构建一个具备实时网页数据抓取、结构化分析与自动化报告生成能力的 AI 工作流。通过简单的 API 调用与 JSON 配置&#xff…

牛顿第二定律的所有表达方式:1、线性表达 2、圆形表达 3、双曲线表达 4、抛物线表达5、数列表达

牛顿第二定律是经典力学中的核心定律&#xff0c;表述为&#xff1a;物体的加速度与所受合力成正比&#xff0c;与质量成反比&#xff0c;方向与合力方向相同。其基本矢量形式为&#xff1a; F⃗ma⃗ \vec{F} m \vec{a} Fma 其中&#xff0c;F⃗\vec{F}F 是合力&#xff08;单…

【开发日记】SpringBoot 实现支持多个微信小程序的登录

在实际业务场景中&#xff0c;需要一个后台同时支持多个微信小程序的登录。例如&#xff0c;企业有多个不同业务的小程序&#xff0c;但希望统一在同一个后台系统里进行用户认证和数据处理。这时候&#xff0c;我们就需要一个灵活的方式来管理多个小程序的 appid 和 secret&…

Docker 容器(一)

Docker一、Docker是什么1.什么是Docker2.Docker特点3.比较虚拟机和容器二、Docker安装1.Docker​​三大核心组件​​2.安装步骤&#xff08;Ubuntu&#xff09;3.阿里云镜像加速三、Docker镜像1.什么是镜像2.UnionFS&#xff08;联合文件系统&#xff09;3.Docker镜像加载原理4…

容器安全实践(二):实践篇 - 从 `Dockerfile` 到 Pod 的权限深耕

在上一篇《容器安全实践&#xff08;一&#xff09;&#xff1a;概念篇》中&#xff0c;我们深入探讨了容器安全的底层原理&#xff0c;并纠正了“容器天生安全”的误解。我们了解了 root 用户的双重身份&#xff0c;以及特权容器的危险性。 然而&#xff0c;仅仅了解这些概念…

c#_数据持久化

数据持久化架构 数据是应用程序的命脉。持久化架构的选择直接决定了应用的性能、可扩展性、复杂度和维护成本。本章将深入探讨.NET生态中主流的数据访问模式、工具和策略&#xff0c;帮助你为你的系统做出最明智的数据决策。5.1 ORM之争&#xff1a;Entity Framework Core深度剖…

996引擎-骰子功能

996引擎-骰子功能 测试NPC QF回调函数 结果 参考资料 在测试NPC播放骰子动画。 播放前需要先设置骰子点数 测试NPC [[骰子的显示顺序和点数 对应 私人变量 D0 D1 D2 D3 D4 D5]] -- NPC入口函数 function main(player)-- 骰子共6个,设置骰子点数后,再执行摇骰子,否则没动画…

Vue 3多语言应用开发实战:vue-i18n深度解析与最佳实践

&#x1f4d6; 概述 Vue 3 国际化&#xff08;i18n&#xff09;是构建多语言应用的核心需求。本文档介绍 Vue 3 中实现国际化的主流方案&#xff0c;包括 vue-i18n、Vite 插件方案和自定义解决方案。 &#x1f3af; 主流方案对比 方案优点缺点适用场景vue-i18n功能完整、生态成…

港口船舶流量统计准确率↑27%!陌讯多模态融合算法实战解析

一、行业痛点&#xff1a;港口船舶流量统计的三大核心难题智慧港口建设中&#xff0c;船舶流量统计是泊位调度、航道管理与安全预警的核心数据支撑&#xff0c;但传统方案受场景特性限制&#xff0c;长期存在难以解决的技术瓶颈。据《2023 年中国港口智能化发展报告》显示&…

Shell脚本的基础知识学习

Shell 脚本是 Linux/Unix 系统的核心自动化工具&#xff0c;能够完成以下任务&#xff1a; &#xff08;1&#xff09;批量操作&#xff1a;一键安装软件、批量处理文件&#xff08;重命名、压缩、备份等&#xff09;。 &#xff08;2&#xff09;系统管理&#xff1a;监控资源…

k8s部署,pod管理,控制器,微服务,集群储存,集群网络及调度,集群认证

k8s部署 k8s中容器的管理方式 ​ Kubernetes集群创建方式 centainerd 默认情况下&#xff0c;K8S在创建集群时使用的方式 docker docker使用的普记录最高&#xff0c;虽然K8S在1.24版本后已经费力了kubelet对docker的支持&#xff0c;但时可以借助cri-docker方式来实现集…

JAVA限流方法

在 Java 项目中限制短时间内的频繁访问&#xff08;即接口限流&#xff09;&#xff0c;是保护系统资源、防止恶意攻击或高频请求导致过载的重要手段。常见实现方案可分为单机限流和分布式限流&#xff0c;以下是具体实现方式&#xff1a;一、核心限流算法无论哪种方案&#xf…

性能比拼: .NET (C#) vs. Fiber (Go)

本内容是对知名性能评测博主 Anton Putra .NET (C#) vs. Fiber (Go): Performance (Latency - Throughput - Saturation - Availability) 内容的翻译与整理, 有适当删减, 相关指标和结论以原作为准 在本视频中&#xff0c;我们将对比 C# 与 .NET 框架和 Golang 的表现。在第一个…

信誉代币的发行和管理机制是怎样的?

信誉代币的发行与管理机制是区块链技术与经济模型深度融合的产物&#xff0c;其核心在于通过代码和社区共识构建可量化、可验证的信任体系。以下从技术架构、经济模型、治理机制三个维度展开分析&#xff0c;并结合具体案例说明&#xff1a;一、发行机制&#xff1a;行为即价值…

神经网络|(十二)概率论基础知识-先验/后验/似然概率基本概念

【1】引言 前序学习进程中&#xff0c;对贝叶斯公式曾经有相当粗糙的回归&#xff0c;实际上如果我们看教科书或者网页&#xff0c;在讲贝叶斯公式的时候&#xff0c;会有几个名词反复轰炸&#xff1a;先验概率、后验概率、似然概率。 今天就来把它们解读一下&#xff0c;为以…

使用UE5开发《红色警戒3》类战略养成游戏的硬件配置指南

从零开始&#xff0c;学习 虚幻引擎5&#xff08;UE5&#xff09;&#xff0c;开始游戏开发之旅&#xff01;本文章仅提供学习&#xff0c;切勿将其用于不法手段&#xff01;开发类似《红色警戒3》级别的战略养成游戏&#xff0c;其硬件需求远超普通2D或小型3D项目——这类游戏…

Vue2+Vue3前端开发_Day12-Day14_大事件管理系统

参考课程: 【黑马程序员 Vue2Vue3基础入门到实战项目】 [https://www.bilibili.com/video/BV1HV4y1a7n4] ZZHow(ZZHow1024) 项目收获 Vue3 composition APIPinia / Pinia 持久化处理Element Plus&#xff08;表单校验&#xff0c;表格处理&#xff0c;组件封装&#xff09…

[ACTF新生赛2020]明文攻击

BUUCTF在线评测BUUCTF 是一个 CTF 竞赛和训练平台&#xff0c;为各位 CTF 选手提供真实赛题在线复现等服务。https://buuoj.cn/challenges#[ACTF%E6%96%B0%E7%94%9F%E8%B5%9B2020]%E6%98%8E%E6%96%87%E6%94%BB%E5%87%BB下载查看&#xff0c;一个压缩包和一张图片。压缩包需要密…