JUC并发编程16 | CAS自旋锁

CAS自旋锁

是什么,干什么,解决了什么痛点?如何解决,如何使用。

原子类:java.util.concurrent.atomic

在没有CAS之前,多线程环境不使用原子类保证线程安全i++等操作,会出现数据问题,如果直接加锁synchronized,资源的开销就比较大

在出现CAS之后,多线程环境,使用原子类保证线程安全i++,类似我们的乐观锁

CAS是什么

CAS是compare and swap的缩写,中文翻译为比较并交换,实现并发算法时常用的一种技术

CAS 包含三个操作数 —— 内存位置、预期原值及更新值

在执行CAS操作的时候,将内存位置的值与预期原值比较,

  • 如果相匹配,那么处理器会自动将该位置值更新为新值
  • 如果不匹配,处理器不做任何操作,多个线程同时执行CAS只有一个会成功

CAS的原理

CAS 有三个操作数,位置内存值V,旧的预期值A,要修改的更新值为B

当且仅当就得预期值A与内存值V相同时,将内存值V修改位B,否则什么都不做,重来——即自旋

在这里插入图片描述

这是通过硬件级别保证的

Unsafe 类

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

它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。

CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。

执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。

进入Unsafe方法查看源码

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

/**
上面三个方法都是类似的,主要对4个参数做一下说明。
var1:表示要操作的对象
var2:表示要操作对象中属性地址的偏移量
var4:表示需要修改数据的期望的值
var5/var6:表示需要修改为的新值
*/

1 Unsafe
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地〈native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,共内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

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

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

// AtomicInteger 类
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

// Unsafe 类
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // volatile 修饰,一旦var5被修改会被立即获知
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

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

假设线程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也通过getlntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  5. 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

原子引用AtomicReference

public class CASDemo {
    public static void main(String[] args) {
        AtomicReference<User> atomicReference = new AtomicReference<>();
        User zhangsan = new User("zhangsan", 22);
        User lisi = new User("lisi", 24);
        atomicReference.set(zhangsan);
        System.out.println(atomicReference.compareAndSet(zhangsan,lisi)+"\t" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(zhangsan,lisi)+"\t" + atomicReference.get().toString());
    }
}

CAS与自旋锁

通过cas操作完成自旋锁,A线程先进来,调用lock方法自己持有锁5秒;

B随后进来发现当前线程支持有所,进行自旋等待,直到A释放锁后B随后抢到

public class CASDemo {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public void lock(){
        Thread thread = Thread.currentThread();
        System.out.println("==============="+Thread.currentThread().getName()+" come in ==============");
        while (!atomicReference.compareAndSet(null,thread)) {}
    }
    public void unlock(){
        Thread thread =  Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println("==============="+Thread.currentThread().getName()+" task is over ==============");
    }
    public static void main(String[] args) throws InterruptedException {
        CASDemo casDemo = new CASDemo();
        new Thread(()->{
            casDemo.lock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                casDemo.unlock();
            }
        },"t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            casDemo.lock();
            try {
                TimeUnit.SECONDS.sleep(8);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                casDemo.unlock();
            }
        },"t2").start();
    }
}

CAS的缺点

  • 循环时间长开销大
  • 具有ABA问题

循环时间长开销大

如果cas失败,会一直进行尝试。如果cas长时间一直不成功,可能会给cpu带来很大的开销

ABA问题

CAS会导致“ABA问题”。

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期OK,然后线程1操作成功。

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

@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
    private String name;
    private int id;
}
public class ABADemo {

    public static void main(String[] args) {

        Book java = new Book("java",1);
        Book mysql = new Book("mysql",2);
        AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(java, 1);
        new Thread(()->{
            // 初始条件是java
            System.out.println(stampedReference.getReference()+"\t初始条件是java:" + stampedReference.getStamp());
            // 此时邮戳莫有启动,但是已经被改为mysql了
            stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp());
            System.out.println(stampedReference.getReference()+"\t被改为mysql了" + stampedReference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改回Java了
            stampedReference.compareAndSet(mysql, java, stampedReference.getStamp(), stampedReference.getStamp());
            System.out.println(stampedReference.getReference()+"\t修改回Java了:" + stampedReference.getStamp());
        },"t1").start();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 不知道被就该过了
            stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp());
            System.out.println(stampedReference.getReference()+"\t不知道被就该过了,此时还能改为mysql" + stampedReference.getStamp());

        },"t2").start();

    }
}
/**
Book(name=java, id=1)	初始条件是java:1
Book(name=mysql, id=2)	被改为mysql了1
Book(name=java, id=1)	修改回Java了:1
Book(name=mysql, id=2)	不知道被就该过了,此时还能改为mysql1
*/

解决:ABA

使用 AtomicStampedReference

内容版本
A1
B2
A3

解决代码

@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
    private String name;
    private int id;
}
public class ABADemo {
    public static void main(String[] args) {
        Book java = new Book("java",1);
        Book mysql = new Book("mysql",2);
        AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(java, 1);
        System.out.println(stampedReference.getReference()+"\t" + stampedReference.getStamp());
        boolean b;
        // 如果是java,且邮戳不变,那就换成mysql,同时邮戳+1
        b = stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp()+1);
        System.out.println(stampedReference.getReference()+"\t" + stampedReference.getStamp());
        // 把 java 换回来
        b = stampedReference.compareAndSet(mysql, java, stampedReference.getStamp(), stampedReference.getStamp()+1);
        System.out.println(stampedReference.getReference()+"\t" + stampedReference.getStamp());
    }
}

上面演示了单线程的情况,下面演示多线程的cas情况

@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
    private String name;
    private int id;
}
public class ABADemo {

    public static void main(String[] args) {

        Book java = new Book("java",1);
        Book mysql = new Book("mysql",2);
        AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(java, 1);
        new Thread(()->{
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t首次版本号:" + stampedReference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 此时邮戳莫有启动,但是已经被改为mysql了
            stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t"+stampedReference.getReference()+"\t版本号2:" + stampedReference.getStamp());

            // 修改回Java了
            stampedReference.compareAndSet(mysql, java, stampedReference.getStamp(), stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t"+stampedReference.getReference()+"\t版本号3:" + stampedReference.getStamp());
        },"t1").start();
        new Thread(()->{
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t首次版本号:" + stampedReference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 被修改过了
            boolean b = stampedReference.compareAndSet(java, mysql, stamp, stampedReference.getStamp()+1);
            System.out.println(b+"\t"+stampedReference.getReference()+"\t" + stampedReference.getStamp());

        },"t2").start();
    }
}
/**

t1	首次版本号:1
t2	首次版本号:1
t1	Book(name=mysql, id=2)	版本号2:2
t1	Book(name=java, id=1)	版本号3:3
false	Book(name=java, id=1)	3
*/

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Day968.如何开启一个遗留系统现代化项目? -遗留系统现代化实战

如何开启一个遗留系统现代化项目&#xff1f; Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于如何开启一个遗留系统现代化项目&#xff1f;的内容。那如何启动一个遗留系统现代化项目。 一、项目背景 说来有点唏嘘&#xff0c;国内遗留系统的重灾区&#xff0c;恰恰…

WiFi(Wireless Fidelity)基础(八)

目录 一、基本介绍&#xff08;Introduction&#xff09; 二、进化发展&#xff08;Evolution&#xff09; 三、PHY帧&#xff08;&#xff08;PHY Frame &#xff09; 四、MAC帧&#xff08;MAC Frame &#xff09; 五、协议&#xff08;Protocol&#xff09; 六、安全&#x…

迪赛智慧数——柱状图(象形标识图):在选择另一半时,你更看重的是?

效果图 好看只排第六&#xff0c;第一确实众望所归&#xff01;当代男女择偶标准出炉&#xff0c;一张图带你看清。 女性挑选另一半时&#xff0c;她们更看重伴侣收入高、职业体面、工作能力强、受教育程度高&#xff0c;还得和自己有共同话题。 男性择偶观和女性恰恰相反&am…

ctfshow周末大挑战2023/5/12

本周周末大挑战用到的函数讲解 parse_url() 作用&#xff1a;解析URL&#xff0c;返回其组成部分 语法&#xff1a; parse_url ( string $url [, int $component -1 ] ) 参数&#xff1a; url&#xff1a;要解析的 URL。无效字符将使用 _ 来替换。 component&#xff1a; …

软件测试月薪2万,需要技术达到什么水平?

最近跟朋友在一起聚会的时候&#xff0c;提了一个问题&#xff0c;说一个软件测试工程师如何能月薪达到二万&#xff0c;技术水平需要达到什么程度&#xff1f;人回答说这只能是大企业或者互联网企业工程师才能拿到。也许是的&#xff0c;小公司或者非互联网企业拿二万的不太可…

Threejs进阶之十四:在uniapp中使用threejs创建三维图形

在uniapp中使用threejs 一、uni-app介绍二、新建uni-app项目三、安装three.js库四、在vue组件中引入three.js库五、创建场景(Scene)和相机(Camera)六、创建渲染器(Renderer)七、创建物体和灯光八、渲染场景(Scene)九、运行测试核心代码 一、uni-app介绍 uni-app是一个基于Vue.…

sqlserver 中的表值函数和标量函数

目录 一、表值函数 1.内联表值函数 1.创建函数 2.调用函数 3.返回结果 2.多语句的表值函数 2.调用函数 3.返回结果 3.内联表值函数和多语句的表值函数的区别 1.语法上 2.结构上 二、标量函数 1.创建函数 2.调用函数 2.返回结果 总结 一、表值函数 表值函数是返回一个Table类型…

如何使用jmeter进行压测

1.概述 一款工具&#xff0c;功能往往是很多的&#xff0c;细枝末节的地方也很多&#xff0c;实际的测试工作中&#xff0c;绝大多数场景会用到的也就是一些核心功能&#xff0c;根本不需要我们事无巨细的去掌握工具的所有功能。所以本文将用带价最小的方式讲解如何快速上手使用…

centos7.5离线安装部署TiDB-6.5.0分布式系统

centos7.5离线安装部署TiDB-6.5.0分布式系统 一、需求&#xff0c;为什么要部署TiDB-6.5.0分布式系统 当前绝大部分企业的业务数据都分散在不同的系统中&#xff0c;没有一个统一的汇总&#xff0c;随着业务的发展&#xff0c;企业的决策层需要了解整个公司的业务状况以便及时…

【Linux Network】应用层协议——HTTP

目录 1. 认识URL 2. urlencode和urldecode urlencode例子&#xff1a; urldecode例子&#xff1a; 3. HTTP协议格式 3.1 HTTP请求&#xff1a; 3.2 HTTP响应&#xff1a; 3.3 HTTP的方法&#xff1a; 3.4 GET方法和POST方法的区别 3.5 HTTP的状态码&#xff1a; 3.6 HTTP常见He…

Buf 教程 - 使用 Protobuf 生成 Golang 代码和 Typescript 类型定义

简介 Buf 是一款更高效、开发者友好的 Protobuf API 管理工具&#xff0c;不仅支持代码生成&#xff0c;还支持插件和 Protobuf 格式化。 我们可以使用 Buf 替代原本基于 Protoc 的代码生成流程&#xff0c;一方面可以统一管理团队 Protoc 插件的版本、代码生成配置&#xff…

QT的qrc文件的创建和编辑

qrc文件&#xff0c;这个是Qt的资源文件&#xff0c;如果在pro文件中不包含的话&#xff0c;在编译的时候会提示找不到相应资源的错误&#xff1b;下面说一下手动修改pro和编写qrc文件的方法: 2.1 添加qrc文件&#xff1b; 2.2 编写qrc文件&#xff1b; 可以用 file…

Linux_证书_Openssl工具详解

文章目录 OpenSSLopenssl实现对称加密openssl生成密钥对、非对称加密、数字签名根据CA颁布证书生成ca私钥和ca证书根据ca生成证书 小结 OpenSSL OpenSSL 是一个开源项目&#xff0c;其组成主要包括一下三个组件&#xff1a; openssl&#xff1a;多用途的命令行工具 libcrypt…

AR VR 到底哪种技术可以改变未来?

随着科技的不断进步&#xff0c;虚拟现实&#xff08;VR&#xff09;和增强现实&#xff08;AR&#xff09;技术已经成为了当今科技领域的热门话题。VR和AR的出现&#xff0c;为人们带来了前所未有的体验和感受&#xff0c;也为各行各业的发展提供了新的机遇。但是&#xff0c;…

clang-format configurator - 交互式创建 clang-format 格式配置文件

clang-format configurator - 交互式创建 clang-format 格式配置文件 clang-format configurator https://zed0.co.uk/clang-format-configurator/ clang-format-configurator https://github.com/zed0/clang-format-configurator Interactively create a clang-format confi…

ANR基础篇 - Trace.txt文件分析

系列文章目录 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 例如&#xff1a;第一章 Python 机器学习入门之pandas的使用 文章目录 系列文章目录前言一、trace.txt文件示例二、日志分析2.1 CPU 负载2.2 内存信息2.3 堆栈信息schedst…

裸辞5个月,面试了37家公司,终于.....

上半年裁员&#xff0c;下半年裸辞&#xff0c;有不少人高呼裸辞后躺平真的好快乐&#xff01;但也有很多人&#xff0c;裸辞后的生活五味杂陈。 面试37次终于找到心仪工作 因为工作压力大、领导PUA等各种原因&#xff0c;今年2月下旬我从一家互联网小厂裸辞&#xff0c;没想…

学习Se-net和Sk-net 附网络简单代码(pytorch)

&#xff08;一&#xff09;Se-net的原理和思路     Se-net严格来说是一个小结构&#xff0c;它可以直接插入已有的网络结构中&#xff0c;帮助原有结构获得更好的效果&#xff0c;如插入Resnet网络中。 Se-net的整个流程如下&#xff1a;     &#xff08;1&#xf…

Cisco 产品下载链接汇总 2023 持续更新中

Cisco 产品链接汇总 2023 持续更新中 IOS-XE, IOS-XR, NX-OS & FXOS based on linux kernel 请访问原文链接&#xff1a;https://sysin.org/blog/cisco/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin.org 本站 Cisco 产品汇…

Java是如何实现双亲委托机制的

Java 是一种面向对象的编程语言&#xff0c;它有一套独特的类加载机制。其中&#xff0c;双亲委托加载机制是 Java 类加载机制中的一个重要概念。本文将介绍 Java 的双亲委托加载机制是如何实现的&#xff0c;并解释其作用和优点。 Java 类加载机制 在 Java 中&#xff0c;类的…