并发专栏-CAS

news2024/11/16 9:41:35

从 Atomic 到 CAS

CAS 知道吗,如何实现? 讲一讲 AtomicInteger,为什么要用 CAS 而不是 synchronized? CAS 底层原理,谈谈你对 UnSafe 的理解? AtomicInteger 的ABA问题,能说一下吗,原子更新引用知道吗? CAS 有什么缺点吗? 如何规避 ABA 问题?

前言

Java 内存模型要保证可见性,原子性和有序性。

在 JDK 5之前 Java 语言是靠 synchronized 关键字保证同步的,但 synchronized 是一种独占锁,是一种悲观锁, 会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁 ,效率不是很高。

Java 虚拟机又提供了一个轻量级的同步机制——volatile(面试必问的 volatile,你真的理解了吗)

但是 volatile 算是乞丐版的 synchronized,并不能保证原子性 ,所以,又增加了java.util.concurrent.atomic包, 这个包下提供了一系列原子类。

1. Atomic 原子类

Atomic 原子类可以保证多线程环境下,当某个线程在执行 atomic 的方法时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个线程执行。Atomic 类在软件层面上是非阻塞的,它的原子性其实是在硬件层面上借助相关的指令来保证的。

alt

Atomic 包中的类可以分成 4 组:

  1. 基本类型:AtomicBoolean,AtomicInteger,AtomicLong
  2. 数组类型:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  3. 引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
  4. 对象的属性修改类型(原子化对象属性更新器) :AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater
  5. JDK1.8 新增(原子化的累加器):DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder、Striped64

以 AtomicInteger 为例了解常用方法

方法描述
get()直接返回值
addAndGet(int)增加指定的数据后返回增加后的数据,相当于 i++
getAndAdd(int)增加指定的数据,返回变化前的数据,相当于 ++i
getAndIncrement()增加1,返回增加前的数据
getAndDecrement()减少1,返回减少前的数据
getAndSet(int)设置指定的数据,返回设置前的数据
decrementAndGet()减少1,返回减少后的值
incrementAndGet()增加1,返回增加后的值
floatValue()转化为浮点数返回
intValue()转化为int 类型返回
set(int)设置为给定值
lazySet(int)仅仅当get时才会set http://ifeve.com/juc-atomic-class-lazyset-que/
compareAndSet(int, int)尝试新增后对比,若增加成功则返回true否则返回false

Coding~~~

public class CASDemo {
    public static void main(String[] args) {
        System.out.println(num.compareAndSet(67) + "\t + current num:" + num);
        System.out.println(num.compareAndSet(67) + "\t current num:" + num);
    }
}
true  + current num:7
false  current num:7

执行两次结果却不同,Why?

compareAndSet() 尝试新增后对比,若增加成功则返回true否则返回false。其实就是比较并交换,判断用当前值和期望值(第一个参数),是否一致,如果一致,修改为更新值(第二个参数),这就是大名鼎鼎的 CAS。

2. CAS 是什么

2.1 CAS 算法

  • CAS:全称 Compare and swap,即 比较并交换,它是一条 CPU 同步原语。 是一种硬件对并发的支持,针对多处理器操作而设计的一种特殊指令,用于管理对共享数据的并发访问。
  • CAS 是一种 无锁的非阻塞算法的实现。
  • CAS 包含了 3 个操作数:
    • 需要读写的内存值 V
    • 旧的预期值 A
    • 要修改的更新值 B
  • 当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的 值,否则不会执行任何操作(他的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。)

CAS 并发原语体现在 Java 语言中的 sum.misc.Unsafe 类中的各个方法。调用 Unsafe 类中的 CAS 方法, JVM 会帮助我们实现出 CAS 汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于 CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,CAS 是一条 CPU 的原子指令,不会造成数据不一致问题。

我们常用的 java.util.concurrent 包就建立在CAS之上。

2.2 用 CAS 分析 AtomicInteger 类

查看 AtomicInteger 代码如下,可以看到该类下的方法大部分是 调用了 Unsafe

alt

2.2.1 UnSafe 类

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

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

public final class Unsafe {
    private static final Unsafe theUnsafe;
 // ......
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

    public native int getInt(Object var1, long var2);

    public native void putInt(Object var1, long var2, int var4);

    public native Object getObject(Object var1, long var2);

    public native void putObject(Object var1, long var2, Object var4)
    
    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);
    // ......
}

Unsafe 类为一单例实现,提供静态方法 getUnsafe 获取 Unsafe 实例,当且仅当调用 getUnsafe 方法的类为引导类加载器所加载时才合法,否则抛出 SecurityException 异常

2.2.2 valueOffset

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

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

2.2.3 volatile int value

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

2.2.4 举个栗子

我们用线程安全的 ++i 举例,也就是 AtomicInteger 中的 getAndAdd

逐层看 Unsafe 类中的 getAndAdd() 的源码如下

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

解毒

可以看到 getAndAddInt 方法有 4 个参数

  • val1:AtomicInteger 对象本身

  • var2:该对象值的引用地址,内存偏移量

  • var4: 需要变动的数量,即 ++i 的 i

  • var5:用var1, var2 找出的主内存中真实的值(通过内存偏移量)

this.compareAndSwapInt 用该对象当前的值与 var5 比较,如果相同,更新 var5 + var4 并且返回 true,如果不同,继续取值然后再比较,直到更新完成。

这一操作没有加锁,反复执行,既保证了一致性,又保证了并发性

假设线程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进行比较替换,直到成功

2.2.5 使用 UnSafe 类

那如若想使用这个类,该如何获取其实例?有如下两个可行方案

  1. getUnsafe 方法的使用限制条件出发,通过Java命令行命令 -Xbootclasspath/a 把调用 Unsafe 相关方法的类A所在 jar 包路径追加到默认的 bootstrap 路径中,使得A被引导类加载器加载,从而通过 Unsafe.getUnsafe 方法安全的获取 Unsafe 实例。
java -Xbootclasspath/a: ${path}   // 其中path为调用Unsafe相关方法的类所在jar包路径 
  1. 通过反射技术暴力获取 Unsafe 对象

    private static Unsafe reflectGetUnsafe() {
        try {
          Field field = Unsafe.class.getDeclaredField("theUnsafe");
          field.setAccessible(true);
          return (Unsafe) field.get(null);
        } catch (Exception e) {
          log.error(e.getMessage(), e);
          return null;
        }
    }

美团技术团队有一篇介绍Unsafe 类的文章:Java魔法类:Unsafe应用解析

3. CAS 缺点

  • 循环时间长,开销很大。CAS算法需要不断地自旋来读取最新的内存值,长时间读取不到就会造成不必要的CPU开销。

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

  • 只能保证一个共享变量的原子操作

    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

  • ABA 问题

ABA 问题

ABA 问题是什么?是如何产生的?

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

比如线程1从内存位置 V 中取出A,这时线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这个时候线程1进行CAS操作发现内存中仍然是A,线程1就会误认为它没有被修改过,这个漏洞就是CAS操作的"ABA"问题。

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

原子引用

我们平时操作的不止是基本数据类型,大多数情况其实是类对象,Atomic 提供的引用类型 AtomicReference 通过泛型可以支持对对象的原子操作

public class AtomicRefrenceDemo {

    public static void main(String[] args) {

        User tom = new User("tom",18);
        User jim = new User("jim",20);

        AtomicReference<User> user = new AtomicReference<>();
        user.set(tom);

        System.out.println(user.compareAndSet(tom, jim)+"\t"+user.get().toString());
        System.out.println(user.compareAndSet(tom, jim)+"\t"+user.get().toString());

    }
}

class User{
    String name;
    int age;
    public User(String tom, int i) {
    }
}

除了AtomicReference ,Atomic 还提供了AtomicStampedReference、AtomicMarkableReference

解决ABA 问题

各种乐观锁的实现中通常都会用版本戳 version 来对记录或对象标记,避免并发操作带来的问题

在Java中,AtomicStampedReference<V> 也实现了这个作用,它通过包装[E,int]的元组来对对象标记版本戳stamp,从而避免ABA问题

public class AtomicStampedReferenceDemo {

    static AtomicStampedReference<String> asf = new AtomicStampedReference<>("A"1);

    public static void main(String[] args) {

        new Thread(() -> {
            String value = asf.getReference();
            System.out.println("Thread1 current value: " + asf.getReference() + ", stamp: " + asf.getStamp());

            asf.compareAndSet(value, "B", asf.getStamp(), asf.getStamp() + 1);
            System.out.println("Thread1: " + value + "——>" + asf.getReference() + ",stamp:" + asf.getStamp());
            value = asf.getReference();
            asf.compareAndSet(asf.getReference(), "A", asf.getStamp(), asf.getStamp() + 1);
            System.out.println("Thread1: " + value + "——>" + asf.getReference() + ",stamp:" + asf.getStamp());
        }).start();

        new Thread(() -> {
            String value = asf.getReference();

            int stamp = asf.getStamp();
            System.out.println("Thread2 current value: " + asf.getReference() + ", stamp: " + stamp);

            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread2: " + value + "——>" + "B" + ",stamp:" + stamp + 1);
            boolean flag = asf.compareAndSet(value, "B", stamp, stamp + 1);
            if (flag) {
                System.out.println("Thread2 update from " + value + " to B");
            } else {
                System.out.println("Thread2 update fail");
            }
        }).start();
    }
}
Thread1 current value: A, stamp: 1
Thread2 current value: A, stamp: 1
Thread1: A——>B,stamp:2
Thread1: B——>A,stamp:3
Thread2: A——>B,stamp:11
Thread2 update fail

本文由 mdnice 多平台发布

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

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

相关文章

自学黑客(网络安全/Web安全),一般人我还是劝你算了吧

由于我之前写了不少网络安全技术相关的文章&#xff0c;不少读者朋友知道我是从事网络安全相关的工作&#xff0c;于是经常有人私信问我&#xff1a; 我刚入门网络安全&#xff0c;该怎么学&#xff1f; 要学哪些东西&#xff1f; 有哪些方向&#xff1f; 怎么选&a…

自学网络安全(黑客)该如何系统学习

一、自学网络安全学习的误区和陷阱 1.不要试图以编程为基础的学习开始学习 我在之前的回答中&#xff0c;我都一再强调不要以编程为基础再开始学习网络安全&#xff0c;一般来说&#xff0c;学习编程不但学习周期长&#xff0c;而且实际向安全过渡后可用到的关键知识并不多 一…

layui(1)

Layui镜像站-经典开源模块化前端 UI 框架(官方文档完整镜像) 下载框架包 点击文档&#xff0c;可进入学习界面 1.引入框架包 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-C…

『赠书活动 | 第八期』《ChatGpt全能应用一本通》

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; 『赠书活动 &#xff5c; 第八期』 本期书籍&#xff1a;《ChatGpt全能应用一本通》 赠书规则&#xff1a;评论区&#xff1a;点赞&#xff5c;收藏&#xff5c;留言 …

深度学习(卷积神经网络)

文章目录 动物视觉神经&#xff0c;以及脑科学视网膜——视觉第一站外膝体——信息中转站视皮层——中央处理器小tips 人工神经网络神经认知机模型卷积神经网络结构&#xff08;Convolutional Neural Network&#xff0c;CNN&#xff09;卷积层池化层全连接层输出层softmax函数…

智能离网微逆变系统

文章目录 一、功能描述二、硬件部分2.1、单片机选型及中断号2.1.1、引脚分配 2.2、EG80102.3、控制电路图2.4、主电路图 三、代码流程图四、代码部分展示4.1、主函数4.2、modbus 五、项目演示 一、功能描述 把风光能&#xff0c;逆变为可调压调频的交流电可通过串口屏&#xf…

通过python封装采集商品ID请求获取京东商品详情数据,京东商品详情接口,京东API接口

使用Python封装采集商品ID请求获取京东商品详情数据。具体步骤如下&#xff1a; 使用Python中的requests库发送HTTP请求&#xff0c;获取商品ID列表。采集方法可根据需求选择&#xff0c;如爬虫框架Scrapy、Selenium等。导入京东API的Python SDK&#xff0c;如jdapi&#xff0…

YARN【工作机制】

Yarn概念 Yarn 是一个资源调度平台&#xff0c;负责为运算程序提供服务器运算资源&#xff0c;相当于一个分布式 的 操作系统平台 &#xff0c;而 MapReduce 等运算程序则相当于运行于 操作系统之上的应用程序 。 Yarn的四大组件 YARN 主要由 ResourceManager&#xff08;…

javascript基础十九:说说你对正则表达式的理解?应用场景?

一、是什么 正则表达式是一种用来匹配字符串的强有力的武器 它的设计思想是用一种描述性的语言定义一个规则&#xff0c;凡是符合规则的字符串&#xff0c;我们就认为它“匹配”了&#xff0c;否则&#xff0c;该字符串就是不合法的 在 JavaScript中&#xff0c;正则表达式也是…

MySQL架构简介

MySQL是系统架构中最常见的中间件&#xff0c;主要由Server层&#xff08;连接器Connectors、连接池Connection Pool、查询缓存query cache、分析器Parser、优化器Optimizer、执行器、binlog&#xff09;以及存储引擎层组成。 MySQL架构简介 连接器 与客户端建立连接、认证身…

0803平面及其方程-向量代数与空间解析几何

文章目录 1 曲面方程与空间曲线方程的概念1.1 曲面方程1.2 空间曲线的方程 2 平面的点法式方程3 平面的一般方程4 两平面的夹角4.1 两平面夹角的定义4.2 夹角的余弦公式4.3 点到平面的距离 结语 1 曲面方程与空间曲线方程的概念 1.1 曲面方程 如果曲面与三元方程 ​ F ( x …

安捷伦MSOX4104A示波器/Agilent MSO-X4104A

安捷伦MSOX4104A示波器/Agilent MSO-X4104A 简介&#xff1a; 1GHz带宽 4个模拟通道 集成逻辑计时分析仪 配有业界*大的 12.1 英寸电容触摸屏 产品特点&#xff1a; 五合一的仪器 示波器 逻辑分析仪&#xff08;可选&#xff09; 串行协议分析仪&#xff08;USB2.0、ARIN…

柔性车间作业调度

1柔性车间作业调度 n n n个工件 { J 1 , J 2 , ⋯ , J n } \{J_1,J_2,\cdots,J_n\} {J1​,J2​,⋯,Jn​}要在 m m m台机器 { M 1 , M 2 , ⋯ , M m } \{M_1,M_2,\cdots,M_m\} {M1​,M2​,⋯,Mm​}上加工。每个工件包含一道或多道工序&#xff0c;工序顺序是预先确定的&#xf…

【Java|多线程与高并发】Thread 常见的方法总结

文章目录 1. 前言2. 方法getId()3. 方法getName()4. 方法getState()5. 方法getPriority(int newPriority)6. 方法isDaemon()和setDaemon()7. 方法isAlive()8. 方法isInterrupted()9. 方法currentThread()10. 方法sleep()11. 方法join()12. 总结 1. 前言 本文主要介绍Thread类常…

第11章_数据库的设计规范

第11章_数据库的设计规范 1. 为什么需要数据库设计 我们在设计数据表的时候&#xff0c;要考虑很多问题。比如: 用户都需要什么数据?需要在数据表中保存哪些数据?如何保证数据表中数据的正确性&#xff0c;当插入、删除、更新的时候该进行怎样的约束检查?。如何降低数据表…

javascript基础十八:说说你对JavaScript中事件循环的理解​

一、是什么 JavaScript 在设计之初便是单线程&#xff0c;即指程序运行时&#xff0c;只有一个线程存在&#xff0c;同一时间只能做一件事 为什么要这么设计&#xff0c;跟JavaScript的应用场景有关 JavaScript 初期作为一门浏览器脚本语言&#xff0c;通常用于操作 DOM &#…

【C++】C++11新特性的讲解

新特性讲解第一篇~ 文章目录 前言一、较为重要的新特性 1.统一的初始化列表2.decltype关键字3.右值引用移动语义总结 前言 C11 简介 &#xff1a; 在 2003 年 C 标准委员会曾经提交了一份技术勘误表 ( 简称 TC1) &#xff0c;使得 C03 这个名字已经取代了 C98 称为 C11 之前的…

【游戏编程扯淡精粹】工作第三年总结

工作第三年总结 文章目录 工作第三年总结#1 做了什么自研路线Lua 脚本系统ToolX #2 职业发展如何做事技术中台化内卷的职业市场个人成长 #3 心态建设Owner vs 打工人 今年仍然是个人成长视角更多一些&#xff0c;额外新学到的重点是&#xff0c;借助团队力量 先介绍两个词&…

通过自由度比较迭代次数

( A, B )---3*30*2---( 1, 0 )( 0, 1 ) 让网络的输入只有3个节点&#xff0c;AB训练集各由5张二值化的图片组成&#xff0c;让A中有7个1&#xff0c;B中全是0&#xff0c;让A的5行1的数量为1&#xff0c;1&#xff0c;1&#xff0c;2&#xff0c;2&#xff1b;让A的3列1的数量…

chatgpt赋能python:Python列表从后往前删除的方法及注意事项

Python列表从后往前删除的方法及注意事项 Python是一种功能强大而易于使用的编程语言。在Python中&#xff0c;列表是重要的数据类型之一&#xff0c;它可以存储任意类型的数据&#xff0c;例如整数、字符串、浮点数和对象等&#xff0c;而且列表数据可以动态添加或删除。在编…