首页 > 编程语言 >并发编程系列-CAS

并发编程系列-CAS

时间:2023-09-22 11:12:51浏览次数:38  
标签:变量 CAS 编程 long 并发 线程 操作 方法

锁(lock)的代价

锁是用来做并发最简单的方式,其代价也是最高的,Java在JDK1.5之前都是靠synchronized关键字来加锁。但是加锁机制会有如下几个问题:

  • 加锁、释放锁会需要操作系统进行上下文切换和调度延时,在上下文切换的时候,cpu之前缓存的指令和数据都将失效,这个过程将增加系统开销。

  • 多个线程同时竞争锁,锁竞争机制本身需要消耗系统资源。没有获取到锁的线程会被挂起直至获取锁,在线程被挂起和恢复执行的过程中也存在很大开销。

  • 等待锁的线程会阻塞,影响实际的使用体验。如果被阻塞的线程优先级高,而持有锁的线程优先级低,将会导致优先级反转(Priority Inversion)。

乐观锁与悲观锁

悲观锁:是认为别的线程会修改值。 独占锁是一种悲观锁,synchronized就是一种独占锁。synchronized加锁后就能够确保程序执行时不会被其它线程干扰,得到正确的结果。

乐观锁:本质上是乐观的,认为别的线程不会去修改值。如果发现值被修改了,可以再次重试。CAS机制(Compare And Swap)就是一种乐观锁。

Compare And Swap

CAS是一种有名的无锁(lock-free)算法。也是一种现代 CPU 广泛支持的CPU指令级的操作,只有一步原子操作,所以非常快。而且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了。

CAS有三个操作参数:

  1. 内存位置V(它的值是我们想要去更新的)

  2. 预期原值A(上一次从内存中读取的值)

  3. 新值B(应该写入的新值)

CAS的操作过程:将内存位置V的值与A比较(compare),如果相等,则说明没有其它线程来修改过这个值,所以把内存V的的值更新成B(swap),如果不相等,说明V上的值被修改过了,不更新,而是返回当前V的值,再重新执行一次任务再继续这个过程。

所以,当多个线程尝试使用CAS同时更新同一个变量时,其中一个线程会成功更新变量的值,剩下的会失败。失败的线程可以重试或者什么也不做。

简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我这个值现在是多少”。(这段描述引自《Java并发编程实践》)

JVM对CAS的支持

在JDK1.5之前,如果不编写明确的代码就无法执行CAS操作,在JDK1.5中引入了底层的支持,在int、long和对象的引用等类型上都公开了CAS的操作,并且JVM把它们编译为底层硬件提供的最有效的方法,在运行CAS的平台上,运行时把它们编译为相应的机器指令,如果处理器/CPU不支持CAS指令,那么JVM将使用自旋锁。

在原子类变量中,如java.util.concurrent.atomic包下的AtomicXXX,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类。

java.util.concurrent.atomic.AtomicLong源码中的自增getAndIncrement()方法:

    //+1操作
    public final long getAndIncrement() {
        while (true) {
            long current = get();
            long next = current + 1;
            //当+1操作成功的时候直接返回,退出此循环
            if (compareAndSet(current, next))
                return current;
        }
    }


    //调用JNI实现CAS
    public final boolean compareAndSet(long expect, long update) {
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
    }

JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。在jdk1.8后getAndIncrement()方法已经看不到具体代码了,而是封装在unsafe类里面。

CAS优缺点

缺点

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作。

  1. ABA问题。

    因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

    从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  2. 循环时间长开销大。

    自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

  3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

  4. 比较花费CPU资源,即使没有任何争用也会做一些无用功。

  5. 会增加程序测试的复杂度,稍不注意就会出现问题。

优点

  • 可以保证变量操作的原子性;

  • 并发量不是很高的情况下,使用CAS机制比使用锁机制效率更高;

  • 在线程对共享资源占用时间较短的情况下,使用CAS机制效率也会较高。

Java提供的CAS操作类--Unsafe类

从Java5开始引入了对CAS机制的底层的支持,在这之前需要开发人员编写相关的代码才可以实现CAS。在原子变量类Atomic**中(例如AtomicInteger、AtomicLong)可以看到CAS操作的代码,在这里的代码都是调用了底层(核心代码调用native修饰的方法)的实现方法。在AtomicInteger源码中可以看getAndSet方法和compareAndSet方法之间的关系,compareAndSet方法调用了底层的实现,该方法可以实现与一个volatile变量的读取和写入相同的效果。在前面说到了volatile不支持例如i++这样的复合操作,在Atomic*中提供了实现该操作的方法。JVM对CAS的支持通过这些原子类(Atomic)暴露出来,供我们使用。

而Atomic系类的类底层调用的是Unsafe类的API,Unsafe类提供了一系列的compareAndSwap*方法,下面就简单介绍下Unsafe类的API:

  • long objectFieldOffset(Field field)方法:返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该Unsafe函数中访问指定字段时使用。如下代码使用Unsafe类获取变量value在AtomicLong对象中的内存偏移。
static {
try {
    valueOffset = unsafe.objectFieldOffset
        (AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
  • int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址。

  • int arrayIndexScale(Class arrayClass)方法:获取数组中一个元素占用的字节。

  • boolean compareAndSwapLong(Object obj, long offset, long expect, long update)方法:比较对象obj中偏移量为offset的变量的值是否与expect相等,相等则使用update值更新,然后返回true,否则返回false。

  • public native long getLongvolatile(Object obj, long offset)方法:获取对象obj中偏移量为offset的变量对应volatile语义的值。

  • void putLongvolatile(Object obj, long offset, long value)方法:设置obj对象中offset偏移的类型为long的field的值为value,支持volatile语义。

  • void putOrderedLong(Object obj, long offset, long value)方法:设置obj对象中offset偏移地址对应的long型field的值为value。这是一个有延迟的putLongvolatile方法,并且不保证值修改对其他线程立刻可见。只有在变量使用volatile修饰并且预计会被意外修改时才使用该方法。

  • void park(boolean isAbsolute, long time)方法:阻塞当前线程,其中参数isAbsolute等于false且time等于0表示一直阻塞。time大于0表示等待指定的time后阻塞线程会被唤醒,这个time是个相对值,是个增量值,也就是相对当前时间累加time后当前线程就会被唤醒。如果isAbsolute等于true,并且time大于0,则表示阻塞的线程到指定的时间点后会被唤醒,这里time是个绝对时间,是将某个时间点换算为ms后的值。另外,当其他线程调用了当前阻塞线程的interrupt方法而中断了当前线程时,当前线程也会返回,而当其他线程调用了unPark方法并且把当前线程作为参数时当前线程也会返回。

  • void unpark(Object thread)方法:唤醒调用park后阻塞的线程。

下面是JDK8新增的函数,这里只列出Long类型操作。

  • long getAndSetLong(Object obj, long offset, long update)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量volatile语义的值为update。
//这个方法只是封装了compareAndSwapLong的使用,不需要自己写重试机制
public final long getAndSetLong(Object var1, long var2, long var4) {
 long var6;
 do {
     var6 = this.getLongVolatile(var1, var2);
 } while(!this.compareAndSwapLong(var1, var2, var6, var4));

 return var6;
}
  • long getAndAddLong(Object obj, long offset, long addValue)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量值为原始值+addValue,原理和上面的方法类似。

总结

可以用CAS在无锁的情况下实现原子操作,但要明确应用场合,非常简单的操作且又不想引入锁可以考虑使用CAS操作,当想要非阻塞地完成某一操作也可以考虑CAS。不推荐在复杂操作中引入CAS,会使程序可读性变差,且难以测试,同时会出现ABA问题。

顶尖架构师栈

关注回复关键字

【C01】超10G后端学习面试资源

【IDEA】最新IDEA激活工具和码及教程

【JetBrains软件名】 最新软件激活工具和码及教程

工具&码&教程

转载于:https://mp.weixin.qq.com/s/ELc_9k3QwEHdse_yFatDPw

标签:变量,CAS,编程,long,并发,线程,操作,方法
From: https://www.cnblogs.com/dc-s/p/17721849.html

相关文章

  • 已解决java.lang.ClassCastException: class java.lang.Integer cannot be cast to cl
    已解决java.lang.ClassCastException:classjava.lang.Integercannotbecasttoclassjava.lang.Long文章目录报错问题解决方法声明报错问题之前在工作中遇到过这个坑,记录一下问题以及解决方法,不一定针对所有情况都能用,但是可以供大家参考。问题描述如下:ForkingPickler(file......
  • 一文读懂Java输入输出:编程新手必看
    输出在前面的代码中,我们总是使用System.out.println()来向屏幕输出一些内容。println是printline的缩写,表示输出并换行。因此,如果输出后不想换行,可以用print():注意观察上述代码的执行效果。格式化输出Java还提供了格式化输出的功能。为什么要格式化输出?因为计算机表示的数据不一定......
  • 中小学生数学卷子自动生成程序-结对编程队友互相评价
    中小学数学卷子自动生成程序——结队编程队友互相评价 一、功能实现1、运行程序,显示输入用户姓名及密码,输入姓名和密码之后,自动核验账号密码是否匹配,不匹配的话则要求重新输入账号密码2、在输入账号密码之后登陆成功,登陆成功之后,会要求输入需要生成的试卷的题目数量,题目数量......
  • 箱讯全新上线的AnyCase4.0具体有什么特别之处
    作为领先的全球跨境贸易数智化服务平台,箱讯在为用户提供专业物流服务和专家级供应链解决方案的同时,也在日积月累的沉淀中深挖全球贸易需求,对产品进行持续升级和迭代。2023年9月15日,AnyCase4.0——全球贸易AllinOne集成平台上线啦!集物流服务,外贸服务,供应链金融服务和企业风控服务......
  • GUI编程1
    GUI编程1组件窗口弹窗面板文本框列表框按钮图片监听事件鼠标键盘事件破解工具1、简介Gui的核心技术:SwingAWT1.因为界面不美观。2需要jre环境!为什么我们要学习?可以写出自己心中想要的一些小工具工作时候,也可能需要维护到swing界面,概率极小!了解MVC架构,......
  • c++并发编程实战-第4章 并发操作的同步
    等待事件或等待其他条件坐车案例想象一种情况:假设晚上坐车外出,如何才能确保不坐过站又能使自己最轻松?方法一:不睡觉,时刻关注自己的位置1#include<iostream>2#include<thread>3#include<mutex>4usingnamespacestd;56mutex_mtx;7boolbFlag=false;......
  • Win32编程之通过SetWindowsHookEx注入DLL(十六)
    一、SetWindowsHookEx函数SetWindowsHookEx是用于在Windows操作系统中设置全局或本地的钩子(hook)。钩子是一种用于监视并拦截特定事件或消息的机制,通常用于拦截和处理键盘输入、鼠标操作、窗口消息等。SetWindowsHookEx允许你安装一个全局或本地的钩子过程,以便在事件发生时执行......
  • Lucas定理及其扩展
    Lucas定理定义对于质数\(p\),有:$$\dbinom{n}{m}\modp=\dbinom{n\modp}{m\modp}\dbinom{\lfloor\frac{n}{p}\rfloor}{\lfloor\frac{m}{p}\rfloor}\modp$$由于\(n\modp\)和\(m\modp\)都比模数\(p\)小,可以预处理,而\(\tbinom{\lfloor\frac{n}......
  • casbin-权限控制
    Casbin元模型:PERM模型P(策略)={sub,obj,act,eft}一般存储到数据库Sub:访问实体,一般是用户或者角色,可以用来表示用户的身份信息object:被访问的资源,例如文件,数据库表等act:访问的方法,例如读取,写入。eft:策略结果,一般为空,默认指定allow。E(影响):决定我们是否放行R(请求):requ......
  • 结对编程博客
    结对编程队友:软件2103黄晖凯项目结构如下图通过项目结构可以清晰的看出每个JAVA类的实现功能,便于查阅和修改代码,这是优点。 为了满足个人项目要求建立的抽象类,过于简单抽象, 在主方法后面加上throwsIOException,确保在出现异常的情况下不会崩溃,无法处理,使得代码更加健康。......