JUC并发编程第八篇,谈谈你对CAS的理解?自旋锁,CAS底层原理和存在的问题?

news2024/9/20 22:43:44

JUC并发编程第八篇,谈谈你对CAS的理解?自旋锁,CAS底层原理和存在的问题?

    • 一、CAS是什么?
    • 二、CAS的底层原理,如何理解UnSafe类?
      • 比较:i++线程不安全,那 atomicInteger.getAndIncrement()呢?
    • 三、AtomicReference 原子引用
    • 四、什么事自旋锁,如何自己实现一个自旋锁?
    • 五、CAS存在的问题?
      • 1. 长时间循环开销大
      • 2. ABA问题

一、CAS是什么?

  • CAS(compare and swap),比较并交换,是实现并发算法时常用到的一种技术。
  • 它包含三个操作数——内存位置V、预期原值A及更新值B。
  • 执行CAS操作的时候,会将内存位置的值与预期原值比较:如果相匹配,那么处理器会自动将该位置值更新为新值,如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。
    在这里插入图片描述
public class Demo
{
	//===============多线程环境不使用原子类保证线程安全==================
    volatile int number = 0;
    //读取
    public int getNumber(){
        return number;
    }
    //写入加锁保证原子性
    public synchronized void setNumber(){
        number++;
    }
    //==============多线程环境使用原子类保证线程安全===================
    AtomicInteger atomicInteger = new AtomicInteger();

    public int getAtomicInteger(){
        return atomicInteger.get();
    }

    public void setAtomicInteger(){
        atomicInteger.getAndIncrement();
    }
}

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证比较-更新的原子性,效率更高且更可靠。

  • 它是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
  • 执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现的, 其实在这一点上还是有排他锁的,只是比起用synchronized, 这里的排他时间要短的多, 所以在多线程情况下性能会比较好。
public class CASDemo{
    public static void main(String[] args) throws InterruptedException{
    	//初始值 5
        AtomicInteger atomicInteger = new AtomicInteger(5);
		//比较并交换
        System.out.println(atomicInteger.compareAndSet(5, 2020)+"\t"+atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t"+atomicInteger.get());
    }
}

在这里插入图片描述
compareAndSet 底层源码
在这里插入图片描述
在这里插入图片描述

  • 上面三个方法的4个参数含义如下:
    • var1:表示要操作的对象
    • var2:表示要操作对象中属性地址的偏移量
    • var4:表示需要修改数据的期望的值
    • var5/var6:表示需要修改为的新值

经过查看源码,找到这几个方法,存在于一个叫UnSafe的类中,UnSafe类是什么呢?

二、CAS的底层原理,如何理解UnSafe类?

  • Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。
    在这里插入图片描述

  • Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,Java中CAS的操作执行依赖于Unsafe类的方法。

  • 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

  • 变量value用volatile修饰,保证了多线程之间的内存可见性。

  • 注意:Unsafe类中基本所有方法都是native修饰的,也就是说Unsafe类中的方法都是直接调用操作系统底层资源执行相应任务的。

比较:i++线程不安全,那 atomicInteger.getAndIncrement()呢?

  • CAS是一条CPU并发原语,它用来判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
  • AtomicInteger 类主要利用 CAS + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
    在这里插入图片描述
    在这里插入图片描述
  • CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。
  • 由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

OpenJDK源码里面查看下Unsafe.java,分析 getAndAddInt 操作
在这里插入图片描述

  • 假设线程A和线程B两个线程同时执行getAndAddInt操作(不同CPU):

    • 1、AtomicInteger 里面的 value 原始值为 3,即主内存中 AtomicInteger 的 value 为 3,根据JMM模型,线程A和线程B各自持有一份值为 3 的 value 的副本分别到各自的工作内存。
    • 2、线程A通过 getIntVolatile(var1, var2) 拿到value值 3,这时线程A被挂起。
    • 3、线程B也通过 getIntVolatile(var1, var2) 方法获取到 value 值 3,此时刚好线程B没有被挂起并执行compareAndSwapInt 方法比较内存值也为 3,成功修改内存值为 4,线程B完成。
    • 4、这时线程A恢复,执行 compareAndSwapInt 方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
    • 5、线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行 compareAndSwapInt 进行比较替换,直到成功。

总结:CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性实现方式是基于硬件平台的汇编指令,思想是比较要更新变量的值V和预期值E,相等才会将V的值设为新值N,如果不相等自旋再来。

三、AtomicReference 原子引用

  • AtomicInteger是原子整型,有其它原子类型吗?有,举例如下:
//User
class User{
    String userName;
    int    age;

    public User(String userName, int age) {
        this.userName = userName;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "userName='" + userName + '\'' +
                ", age=" + age +
                '}';
    }
}

//使用示例
public class AtomicReferenceDemo{
    public static void main(String[] args){
        User z3 = new User("z3",24);
        User li4 = new User("li4",26);

        AtomicReference<User> atomicReferenceUser = new AtomicReference<>();

        atomicReferenceUser.set(z3);
        System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t"+atomicReferenceUser.get().toString());
        System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t"+atomicReferenceUser.get().toString());
										        
    }
}

在这里插入图片描述

四、什么事自旋锁,如何自己实现一个自旋锁?

  • 自旋锁是指 尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。
  • 这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

实现一个自旋锁

public class SpinLockDemo {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t come in");
        while(!atomicReference.compareAndSet(null,thread)) { }
    }

    public void myUnLock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"\t myUnLock over");
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();

        new Thread(() -> {
            spinLockDemo.myLock();
            try { TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) { e.printStackTrace(); }
            spinLockDemo.myUnLock();
        },"A").start();

        //暂停一会儿线程,保证A线程先于B线程启动并完成
        try { TimeUnit.SECONDS.sleep( 1 ); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            spinLockDemo.myLock();
            spinLockDemo.myUnLock();
        },"B").start();

    }
}

在这里插入图片描述
通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到。

五、CAS存在的问题?

1. 长时间循环开销大

  • 比如上边的 getAndAddInt 方法,里边有一个 do while 循环,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

2. ABA问题

  • CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差里会导致数据的变化。
  • 比如说一个线程 t1 从内存位置V中取出A,这时候另一个线程 t2 也从内存中取出A,线程 t2 先将V位置的数据变成了B,然后又将V位置的数据变成A,这时候线程 t1 进行CAS操作发现内存中仍然是A,然后线程 t1 操作成功。

尽管线程 t1 的CAS操作成功,但是不代表这个过程就是没有问题的。

如何解决:AtomicStampedReference 携带版本号的引用类型原子类

public class ABADemo {
    static AtomicInteger atomicInteger = new AtomicInteger(100);

    static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);

    public static void main(String[] args) {

        new Thread(() -> {
            //A -> B -> A
            atomicInteger.compareAndSet(100,101);
            atomicInteger.compareAndSet(101,100);
        },"t1").start();

        new Thread(() -> {
            //暂停一会儿线程
            try { Thread.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); };
            //ABA执行成功了
            System.out.println(atomicInteger.compareAndSet(100, 2019)+"\t"+atomicInteger.get());
        },"t2").start();

        //暂停一会儿线程,main彻底等待上面的ABA出现演示完成。
        try { Thread.sleep( 2000 ); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println("============以下是ABA问题的解决(atomicStampedReference)=============================");

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp);//1
            //暂停一会儿线程,
            try { Thread.sleep( 1000 ); } catch (InterruptedException e) { e.printStackTrace(); }
            
            atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 2次版本号:"+atomicStampedReference.getStamp());
            
            atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 3次版本号:"+atomicStampedReference.getStamp());
        },"t3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp);//1
            
            //暂停一会儿线程,获得初始值100和初始版本号1,故意暂停3秒钟让t3线程完成一次ABA操作产生问题
            try { Thread.sleep( 3000 ); } catch (InterruptedException e) { e.printStackTrace(); }
            
            boolean result = atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1);
            System.out.println(Thread.currentThread().getName()+"\t"+result+"\t"+atomicStampedReference.getReference());
        },"t4").start();
    }
}

![在这里插入图片描述](https://img-blog.csdnimg.cn/ce4cd0cf330c411bbbfd8408814917d2.png
也可以使用 AtomicMarkableReference,它与 AtomicStampedReference 的区别下一篇会有。

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

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

相关文章

笔记本电脑有必要分盘吗?电脑是分盘好还是不分盘好

电脑分区是指把电脑磁盘划分成多个磁盘分区&#xff0c;不同的磁盘分区用于存储相应类型的数据。许多用户新购置的电脑&#xff0c;打开一看&#xff0c;都会发现&#xff1a;“电脑只有一个C盘&#xff0c;没有其他的磁盘。那么&#xff0c;要不要对电脑分区呢&#xff1f;”笔…

共享车位|基于SpringBoot+vue+node共享车位平台的设计与实现

作者主页&#xff1a;编程千纸鹤 作者简介&#xff1a;Java、前端、Pythone开发多年&#xff0c;做过高程&#xff0c;项目经理&#xff0c;架构师 主要内容&#xff1a;Java项目开发、毕业设计开发、面试技术整理、最新技术分享 收藏点赞不迷路 关注作者有好处 文末获得源码 …

2022-12-6-Cmake工程转VS环境开发

新建工程后目录有 .vs 执行文件目录 x64 baseline.sln 首先新建一个目录&#xff0c;下面分为四个目录分别是dll&#xff0c;idl&#xff0c;include&#xff0c;lib 在include目录下面把所有Cmake工程中的include目录下的文件夹拷过来 在x64的debug下面把所有的dll动态库拷…

数据比较器,对比数据前后变化细节

前言 在开发的过程中&#xff0c;有时候需要对数据进行比对&#xff0c;来判断是否发生变化。如果一个字段一个字段比较&#xff0c;就太麻烦了。所以通过整合注解与反射的方式&#xff0c;实现一个通用的实体数据比较框架。 设计 使用注解&#xff0c;确定需要比较的属性。…

进程管理笔记

查看进程详情 命令&#xff1a;ps -aux 查看进程 能够观察所有系统的数据 命令&#xff1a;ps la | head -5 命令&#xff1a;ps axjf | head -20 仅查看自己的bash相关的进程 命令&#xff1a;ps l 观察系统所有进程 命令&#xff1a;ps aux 观察进程变化命令 - top …

微服务框架 SpringCloud微服务架构 21 RestClient 操作文档 21.3 更新文档

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 SpringCloud微服务架构 文章目录微服务框架SpringCloud微服务架构21 RestClient 操作文档21.3 更新文档21.3.1 更新文档21 RestClient 操作文…

Python:函数进阶

目录 一、Python中的推导式 需求一 需求二 二、Python的全局作用域 三、Python的多参数传递 四、Python的装饰器 被装饰的方法不带参数 被装饰的方法带参数 带参数的装饰器 一、Python中的推导式 列表生成式是python内置的一种创建列表的方法&#xff0c;通过在[ ]内部执…

会话跟踪技术(Cookie和Session)

目录概述Cookie基本使用Cookie原理Cookie 存活时间Session基本使用Session原理Session使用细节Seesion 销毁&#xff1a;Cookie和Session的对比最后概述 会话&#xff1a; 用户打开浏览器&#xff0c;访问web服务器的资源&#xff0c;会话建立&#xff0c;直到有一方断开连接&…

Mac系统安装Kafka 3.x及可视化工具

写在前面&#xff1a;博主是一只经过实战开发历练后投身培训事业的“小山猪”&#xff0c;昵称取自动画片《狮子王》中的“彭彭”&#xff0c;总是以乐观、积极的心态对待周边的事物。本人的技术路线从Java全栈工程师一路奔向大数据开发、数据挖掘领域&#xff0c;如今终有小成…

网络工程师备考3章

注&#xff1a;本章考察较少&#xff0c;冲刺阶段可直接跳过 最常考点&#xff1a;帧中继&#xff0c;HDLC 3.1 公共交换电话网 英文&#xff1a;Public Switched Telephone Network ,PSTN 这种主网架构已经被淘汰了&#xff0c;现在的电话骨干网都是数字信号&#xff0c;目…

web课程设计网页规划与设计 基于HTML+CSS+JavaScript制作智能停车系统公司网站静态模板

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

Java中三种I/O模型 BIO,NIO,AIO

UNIX 系统下&#xff0c; IO 模型一共有 5 种&#xff1a; 同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。 这也是我们经常提到的 5 种 IO 模型 &#xff08;1&#xff09;同步阻塞I/O模型 应用程序发起read调用后&#xff0c;一直处于阻塞状态 内…

BP综述:自闭症中基于功能连接体的预测模型

自闭症是一种异质性的神经发育疾病&#xff0c;基于功能磁共振成像的研究有助于推进我们对其对大脑网络活动影响的理解。我们回顾了使用功能连接和症状的测量的预测建模如何帮助揭示对这种情况的关键见解。我们讨论了不同的预测框架如何进一步加深我们对复杂自闭症症状学基础的…

Word处理控件Aspose.Words功能演示:在 Python 中将 Word DOCX 或 DOC 转换为 PDF

Word 到PDF是最流行和执行最广泛的文档转换之一。DOCX或DOC文件在打印或共享之前会转换为 PDF 格式。在本文中&#xff0c;我们将在 Python 中自动将 Word 转换为 PDF。步骤和代码示例将演示如何使用 Python 将 Word DOCX 或 DOC 转换为 PDF。此外&#xff0c;您将了解自定义 W…

Stable Diffusion模型阅读笔记

Stable Diffusion模型 什么是Stable Diffusion模型 一般而言&#xff0c;扩散是在图像中反复添加小且随机的噪声。与之相反&#xff0c;Stable Diffusion模型是一种将噪声生成为图像的机器学习模型。经过训练&#xff0c;它可逐步对随机高斯噪声进行去噪以获得感兴趣的样本&a…

Apache Hop Transforms Samples【持续完善中】

Samples transforms 1、abort-basic.hpl 根据筛选器行转换的结果中止此管道 第一步:添加Data grid转换,Meta部分维护字段。 Data部分维护数据,如下图。 第二步:添加filter rows,如下图 按照如下截图进行修改: 第三步:添加Abort 第四步:执行截图如下:

[附源码]Python计算机毕业设计Django学生综合数据分析系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

Golang 【basic_leaming】fmt.Print, fmt.Printf, fmt.Println 的区别

阅读目录fmt 包fmt.Print 原样输出fmt.Printf 格式输出fmt.Println 值 空格 输出换行输出Println 和 Print 区别Println 和 Printf 区别变量类型推导方式定义变量使用 Printf打印一个变量的类型fmt 包 fmt format&#xff0c;是一种格式化输出函数汇总包&#xff0c;用于格式…

这款国产API工具也太强了吧!让我放弃了postman

为什么弃用postman转用Eolink? 程序员在接口开发完成后都通常需要自测&#xff0c;当返回结果根据符合预期时&#xff0c;则表示代表接口可用。自己以前用的是postman来进行接口测试&#xff0c;但postman只能进行接口测试&#xff0c;有局限性、很多场景不能满足我的需求&am…

jenkins集成sonarqube

一、在linux上面安装sonarqube 相关文件下载地址 &#xff08;1&#xff09;下载sonarqube-9.6.1.59531.zip解压 &#xff08;2&#xff09;创建用户 useradd sonar passwd sonar(3)赋权 chown -R sonar.sonar /opt/sonarqube/sonarqube-9.6.1.59531(4)切换用户&#xff0c…