【多线程】CAS原理

news2024/10/11 22:54:42

文章目录

  • 为什么会出现`CAS`思想?
  • `CAS`概念
  • `CAS`自旋
    • 概念
    • `CAS`的简单使用
    • `CAS`源码解析
  • UnSafe类
  • `CAS`底层原理
    • `CAS`的硬件保证
    • `CAS`自旋锁的实现
      • 前置知识----原子引用`AtomicReference`
      • 实现自旋锁
  • CAS缺点
  • `ABA`问题
    • 什么是`ABA`问题
      • 如何解决`ABA`问题
        • 简单案例
        • `AtomicStampedReference`的源码分析

为什么会出现CAS思想?

在没有CAS思想之前,虽然我们可以使用synchronized关键字实现锁,但是synchronized属于悲观锁,降低了程序运行的效率.

synchronized关键字在Java用于实现悲观锁,也称为互斥锁,它确保在同一时刻只有一个线程可以访问特定的代码段或资源
悲观锁的基本思想:在访问资源之前先尝试获取锁,如果锁被其他线程持有,则当前线程会阻塞等待直到锁释放
悲观锁的效率并不高,特别是在并发访问频繁并且锁竞争激烈的情况下,因为每次都需要尝试获取锁,这可能会导致频繁的上下文切换和线程阻塞,增加了系统的开销和延迟。此外,如果多个线程同时请求锁,它们可能会顺序地逐个获取,而不是并行处理,这限制了并发性能。

所以在这种情况下,我们希望可以得到一种既可以保证原子性,又不会影响效率的方法.
通俗来说,就是在不加锁的情况下,依旧可以实现原子性.

CAS概念

  • CAS全称:compare and swap,中文翻译成比较并交换,实现并发算法时常用到的一种技术
  • CAS包含三个操作数:内存位置,预期原值更新值
  • 在执行CAS操作的时候,将内存位置的值与预期值进行比较:
    • 如果相匹配,那么处理器会自动将该位置值更新为新值;
    • 如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功.

CAS自旋

概念

我们先来看一下正常情况下的数据进行修改:
在这里插入图片描述
我们来分析一下正常情况下具体的流程:

  • 线程1,线程2,线程3均会对 内存中的5 进行修改,所以此时线程1 2 3 均会将内存中的5拷贝到自己的工作内存中进行计算(进行自增操作)
  • 此时线程1会进行内存位置的值与预期原值的比较,线程1预期原值为5,此时内存位置的值也是5,相匹配
  • 线程1进行自增后为6,处理器会自动将内存位置的值更新为6
  • 共享数据内存中的数据同步成功

那么在什么情况下会发生CAS自旋呢?
在这里插入图片描述
也就是说,此时在线程1中进行自增操作之后,线程1进行了预期原值内存地址数据的比较,发现不相等,说明在此之前线程23进行了自增并且同步到了内存中.在这种情况下,我们就需要执行CAS自旋
CAS自旋:线程1重新读取内存地址的数据,直到预期原值内存地址数据相同,内存地址的数据才能够修改.
总结如图:
在这里插入图片描述
其中:

  • E:内存中当前的值
  • N:在线程对数据进行操作后的数据
  • V:要更新的数据值

CAS的简单使用

这里我们使用原子类中的AutomicInteger来举例;
AutomicInteger中,存在一个方法:

public final boolean compareAndSet(int expectedValue, int newValue) {
        return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
    }

参数:

  • expectedValue:表示预期原值
  • newValue:表示将要更新的值

public class CasTest {
    public static void main(String[] args) {
        AtomicInteger integer=new AtomicInteger(5);
        integer.compareAndSet(5,10);
        System.out.println(integer.get());
    }
}

运行结果:
在这里插入图片描述
表示修改数据成功

CAS源码解析

这里我们通过AutomicInteger中的自增方法进入源码中观察其中的CAS是如何实现的:

public class CasTest {
    public static void main(String[] args) {
        AtomicInteger integer=new AtomicInteger(5);
        integer.getAndIncrement();
        System.out.println(integer);
    }
}

这里我们找到了CAS实现:

@IntrinsicCandidate
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
        //取出当前内存地址中的数据
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
    }

这里使用到了do{}while()结构,所以在数据没有自加成功的情况下,会一直进行自旋操作

这里的getIntVolatile是一个Native方法,作用是取出当前内存地址中的数据

@IntrinsicCandidate
    public native int getIntVolatile(Object o, long offset);

这里的weakCompareAndSetInt中调用了compareAndSetInt方法,作用是

@IntrinsicCandidate
    public final boolean weakCompareAndSetInt(Object o, long offset,
                                              int expected,
                                              int x) {
        return compareAndSetInt(o, offset, expected, x);
    }

这里的compareAndSetIntNative方法,作用是使用CAS自旋的方式,安全实现对数据的修改操作

@IntrinsicCandidate
    public final native boolean compareAndSetInt(Object o, long offset,
                                                 int expected,
                                                 int x);

UnSafe类

CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地方法(Native)来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据.Unsafe类在sum.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为JavaCAS操作的执行依赖于Unsafe类的方法
我们来看一看AtomicInteger.java 源码中的 Unsafe

public class AtomicInteger extends Number implements java.io.Serializable {
//.......
private volatile int value;//这里使用volatile来修饰,保证对所有线程均可见
public AtomicInteger(int initialValue) {
    value = initialValue;
}
public final int get() {
    return value;
}
private static final Unsafe U = Unsafe.getUnsafe();//AtomicInteger 核心的Unsafe类初始化
private static final long VALUE//初始化Value:是对象在对象实例中的偏移量,获取到主内存中的数据
    = U.objectFieldOffset(AtomicInteger.class, "value");
//compareAndSet:主内存中的数据==expectedValue,将主内存中的数据修改为newValue,不相同,则不修改
public final boolean compareAndSet(int expectedValue, int newValue) {
    //this:对象实例
    //VALUE:主内存中数据的偏移量
    //expectedValue:预期原值
    //newValue:修改的新值
    return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}
}

Unsafe

public final class Unsafe {
//通过对指针进行偏移,不仅可以直接修改指针指向的数据(即使它们是私有的),
//甚至可以找到JVM已经认定为垃圾、可以进行回收的对象
//objectFieldOffset获取非静态属性Field在对象实例中的偏移量
private native long objectFieldOffset1(Class<?> c, String name);
public long objectFieldOffset(Class<?> c, String name) {
    if (c == null || name == null) {
        throw new NullPointerException();
    }
    //返回对象在对象实例中的偏移量
    return objectFieldOffset1(c, name);
}
@IntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
                                             int expected,
                                             int x);
}

CAS底层原理

CAS的硬件保证

CASJDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性.

CAS一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致的问题,Unsafe提供的CAS方法底层实现即为CPU指令cmpxchg.
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后就会执行CAS操作,也就是说CAS原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以多线程情况下性能会比较好.

CAS自旋锁的实现

前置知识----原子引用AtomicReference

使用AtomicReference类,我们可以自己设计一个具备原子性的类,获得各种原子性的方法

实现自旋锁

使用CAS原理,我们自行实现自旋锁
要求:

  1. 线程A进入后,先拿到锁(Lock),释放锁
  2. 线程B进入之后,一直等待线程A释放锁,线程A释放锁之后,线程B拿到锁
public class SpinLock {
    AtomicReference<Thread> atomicReference=new AtomicReference<>();

    //拿到锁
    public void Lock() throws InterruptedException {
        Thread curThread=Thread.currentThread();
        while(!atomicReference.compareAndSet(null,curThread)){//如果当前值不是null,就等待获取锁
            System.out.println(curThread.getName()+" : 正在等待锁");
            TimeUnit.SECONDS.sleep(1);
        }
        //得到锁
        System.out.println(curThread.getName()+"获取到锁");

        TimeUnit.SECONDS.sleep(5);
    }

    //释放锁
    public void unLock(){
        Thread curThread = Thread.currentThread();
        atomicReference.compareAndSet(curThread,null);
        System.out.println(curThread.getName()+"释放掉锁");

    }


    public static void main(String[] args) throws InterruptedException {
        SpinLock spinLock=new SpinLock();
        Thread t1=new Thread(()->{
            try {
                spinLock.Lock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLock.unLock();
        },"A");
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        Thread t2=new Thread(()->{
            try {
                spinLock.Lock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLock.unLock();
        },"B");
        t2.start();
    }
}

运行结果:
在这里插入图片描述
运行结果表示:在A得到锁之后,B需要一直等待锁被A释放,才能得到锁.

这里的AtomicReference<Thread> atomicReference=new AtomicReference<>();相当于一个锁,其中原子类Thread相当于当前哪个线程持有该锁
atomicReference.compareAndSet(null,curThread)相当于判断当前的锁是否被持有.

CAS缺点

  1. 循环时间开销很大:getAndAddInt方法执行时,有一个do while,如果CAS失败,就会一直循环,进行尝试.如果CAS长时间不成功,就会给CPU带来很大的开销
  2. ABA问题:下节详解

ABA问题

什么是ABA问题

在这里插入图片描述

如何解决ABA问题

JUC.Atomic的包中,存在一个AtomicStampedReference的类,这个类就是版本号时间戳原子引用类,这个类记录了主内存中每一次数据的改变,即改变一次,版本号自增1,这样便可以在值相同的情况下,也能判断出值是否发生过更新.
AtomicStampedReference中存在的一些常用API:
在这里插入图片描述

简单案例
public static void main(String[] args) {
        Book book1=new Book(1,"Java");
        Book book2=new Book(2,"MySQL");
        AtomicStampedReference<Book> atomicStampedReference=new AtomicStampedReference<>(book1,1);
        //AtomicStampedReference<Book> atomicStampedReference2=new AtomicStampedReference<>(book2,2);
        if(atomicStampedReference.compareAndSet(book1,book2,1,2));
        System.out.println(book2);
        System.out.println(atomicStampedReference.getReference());
        System.out.println(atomicStampedReference.getStamp());
    }

运行结果:
在这里插入图片描述

我们重点学习其中的public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp)方法
参数:

  • V expectedReference:预期的原子引用
  • V newReference:替换的新的原子引用
  • int expectedStamp:预期的版本戳
  • int newStamp:新的版本戳
AtomicStampedReference的源码分析
public class AtomicStampedReference<V> {

    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }
	
    private volatile Pair<V> pair;
    //CAS方法:
//expectedReference:期待引用
//newReference:修改的新引用
//expectedStamp:期待的版本号
//newStamp::修改的新版本号
public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    return
    	//预期的引用等于当前的引用+预期的版本戳等于当前的版本戳
        expectedReference == current.reference &&
        expectedStamp == current.stamp   &&
        //将新的引用和新的版本进行替换
        ((newReference == current.reference &&
          newStamp == current.stamp) 
          ||casPair(current, Pair.of(newReference, newStamp)));

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

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

相关文章

leetcode链表(二)-两两交换链表中的节点

题目 . - 力扣&#xff08;LeetCode&#xff09; 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。 思路 一定要使用虚拟头节点…

电子学报期刊投稿过程记录

电子学报的编辑老师确实人非常好&#xff0c;专业知识过硬&#xff0c;文章内容审核仔细&#xff0c;对格式的要求相对严格&#xff0c;并且打电话或者邮箱询问都很和善&#xff0c;也很温柔&#xff0c;同时也愿意配合再缴费后提前发送录用证明&#xff0c;但是见刊和网络首发…

单点登录Apereo CAS 7.1客户端集成教程

从上一篇部署并成功运行CAS服务端后,我们已经能通过默认的账号密码进行登录。 上篇地址:单点登录Apereo CAS 7.1安装配置教程-CSDN博客 本篇我们将开始对客户端进行集成。 CAS中的客户端,就是指我们实际开发的各个需要登录认证的应用。现在,跟着笔者的步伐,一起探索如何…

共识算法Raft

引入 在分布式系统中&#xff0c;为了消除单点提高系统可用性&#xff0c;通常会创建副本来进行容错&#xff0c;但这会带来另一个问题就是&#xff0c;如何保证多个副本之间的数据一致性。 为了解决这个问题&#xff0c;计算机行内就提出了共识算法&#xff0c;它允许多个分…

git gui基本使用

一、图形化界面 二、创建新项目 创建文件&#xff0c;加入暂存区&#xff0c;提交到版本库 三、创建分支 四、合并分支 1.切换至master 五、更新分支 六、解决冲突 修改冲突&#xff0c;加入暂存区&#xff0c;提交到版本库 七、远程创建库 Gitee - 基于 Git 的代码托管和研…

低功耗

低功耗 目录 低功耗 STM32中的电源系统 STM32 中的低功耗 相关代码 -- 首先我们先看我们做的项目如何降低功耗 -- 对于设备&#xff0c;功耗怎么降低&#xff1f;把设备上所有的电子模块&#xff0c;都进入低功耗模式。 对于空气质量检测仪&#xff0c;如何降低功耗&…

修改armbian DNS服务器地址(永久修改DNS配置)

linux dns服务器地址的配置文件在/etc/resolv.conf 但系统可能设置的是默认值&#xff0c;也就是192.168.1.1。导致系统无法正常解析域名&#xff0c;进而导致有一些接口无法调用或下载失败。 最直接的思路就是修改/etc/resolv.conf&#xff0c;将其中的nameserver修改为正确的…

必看系列:面试官通过一个问题考查了网络编程所有知识点!

一、写在开头 本文的主题是和大家一起探讨学习:“在浏览器中输入URL开始后,计算机所做的几件事”,这个问题是好几年前自己面试的时候,面试官考问过的,当时准备十分不充分,回答的一塌糊涂,今天拿出来再整理学习一遍,一同进步! 其实这个问题本身倒是不难,但它巧妙的是…

节假日提醒,节假日任务,节假日判断如何做?这篇文章教会你!

你是否有这样的需求&#xff0c;有一个任务需要在大家都休息的时候处理&#xff0c;你肯定会想到周六周日了&#xff0c;那不好意思&#xff0c;遇到调休怎么办呢&#xff1f;遇到国假怎么办呢&#xff1f;我这里所说的节假日和工作日不仅仅指正常的周一至周日&#xff0c;还包…

浙江省发规院产业发展研究所调研组莅临迪捷软件考察调研

2024年10月10日下午&#xff0c;浙江省发展与规划院产业发展研究所调研组一行莅临迪捷软件考察调研&#xff0c;绍兴市府办、区发改、区经信、迪荡街道等相关领导陪同。 调研组一行参观了迪捷软件的展厅与办公区&#xff0c;深入了解了迪捷软件的公司发展历程、运营状况、产品…

Python 如何使用 Bert 进行中文情感分析

前言 在自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;情感分析是一个非常常见且重要的应用。情感分析通常用于识别文本中的情感&#xff0c;例如判断一条微博或评论是正面、负面还是中性。在过去的几年中&#xff0c;随着深度学习的发展&#xff0c;BERT&#xff…

MySQL表的基本操作和数据类型

MySQL表的基本操作和数据类型 表的操作创建表修改表删除表 数据类型数值类型整型浮点型 文本、二进制类型日期时间类型ENUM类型和SET类型 表的操作 创建表 语法&#xff1a; CREATE TABLE table_name(field1 datatype,field2 datatype,field3 datatype )character set 字符集…

Python的matplotlib可视化工具基本操作(数据分析生成图表)

一、安装导入 1、使用包管理器安装matplotlib pip3 install matplotlib 2、导入plt工具 import matplotlib.pyplot as plt 二、基本函数 1、创建图表 使用pyplot工具打点调用创建图表函数 例如创建直方图&#xff1a; import matplotlib.pyplot as plt import pandas…

【unity框架开发9】序列化字典,场景,vector,color,Quaternion

文章目录 前言一、可序列化字典类普通字典简单的使用可序列化字典简单的使用 二、序列化场景三、序列化vector四、序列化color五、序列化旋转Quaternion完结 前言 自定义序列化的主要原因&#xff1a; 可读性&#xff1a;使数据结构更清晰&#xff0c;便于理解和维护。优化 I…

Android Framework默认授予app通知使用权限

安卓通知使用权限 在安卓系统中&#xff0c;应用程序需要获取通知使用权限才能向用户发送通知。以下是关于安卓通知使用权限的一些信息&#xff1a; 权限获取方式 当用户安装应用时&#xff0c;系统可能会在安装过程中提示用户授予应用通知权限。用户可以选择允许或拒绝。 应…

记录一些yolo-world训练数据集的报错

参考的这个文章 https://blog.csdn.net/ITdaka/article/details/138863017?spm1001.2014.3001.5501 openai快捷下载&#xff1a;https://download.csdn.net/download/qq_43767886/89876720 然后我打算训练coco数据集&#xff0c;遇到了以下的问题 问题一 原因&#xff1a;…

智慧农田新篇章:高标准农田灌区信息化的创新实践

在新时代的农业发展蓝图中&#xff0c;智慧农田已成为推动农业现代化、实现可持续发展目标的关键一环。高标准农田灌区信息化作为智慧农业的重要组成部分&#xff0c;正引领着一场深刻的农业技术革命&#xff0c;它不仅关乎粮食安全与资源高效利用&#xff0c;还深刻影响着农村…

writehelpAI论文写作,专业毕业论文救星

撰写专业毕业论文是每位学生学术旅程中的一个重要里程碑&#xff0c;它不仅检验了你对专业知识的掌握程度&#xff0c;还考验着研究能力、批判性思维以及书面表达技巧。在这个过程中&#xff0c;writehelpAI这样的智能写作助手可以成为你的得力伙伴&#xff0c;帮助解决从选题到…

功率检测和语音功能

INA226 INA226 High-Side or Low-Side Measurement, Bi-Directional Current and Power Monitor with I2C Compatible Interface datasheet (Rev. A) INA226功率监测模块原理 7.5 编程 INA226 器件的一个重要方面是它不一定测量电流或功率。该器件可测量施加在 IN 和 IN- 输入…

数据结构重点学习笔记教程——入门必看 数据结构心得

数据结构教程知识点 章节目录 一、数据结构概述二、线性表三、栈与队列四、数组与字符串五、树与二叉树六、图七、排序算法八、查找算法九、哈希表与散列法十、高级数据结构 总结简介 本数据结构教程知识点涵盖了从数据结构概述到高级数据结构的全方位内容&#xff0c;旨在帮…