JUC并发编程(13):CAS机制

CAS机制

参考:

blog.csdn.net/TZ845195485…

CAS底层原理? UnSafe类+CAS思想[自旋锁]

1、CAS的概述?

CAS的全称为Compare-And-Swap,从字面上理解就是比较并更新 ,它是一条CPU并发原语,比较工作内存值(预期值)和主物理内存的共享值是否相同,相同则执行规定操作,否则继续比较直到主内存和工作内存的值一致为止。这个过程是原子的(AtomicInteger类主要利用CAS(compare and swap)+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升)

简单来说:从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。

image.png

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

在 Java的原子类中,有对cas的封装进行应用:此处用 AtomicInteger 举例

代码

package com.company.cas;

import java.util.concurrent.atomic.AtomicInteger;

// CAS compareAndSet : 比较并交换!(unsafe,自旋锁)
//compareAndSet 比较并赋值
public class CAS {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(1);
        // 如果我期望的值达到了,那么就更新,否则,就不更新, CAS 是CPU的并发原语!
        System.out.println(atomicInteger.compareAndSet(1, 2));//true
        System.out.println(atomicInteger.get());//2
        //会失败,因为上面成功交换了
        System.out.println(atomicInteger.compareAndSet(1, 2));//false
        System.out.println(atomicInteger.get());//2
    }
}
复制代码

缺点:

  • 循环会耗时
  • 一次性只能保证一个共享变量的原子性
  • ABA问题

源码解析

点击查看AtomicInteger的源码: 直接通过Unsafe类调用底层 CAS方法

//compareAndSet
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

//getAndIncrement  
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
复制代码

image.png

getAndAddInt方法详解:

image.png

通过一定的轮询,也就是后面介绍的自旋锁:

image.png

CAS锁其又称作Java的乐观锁,但是我们可以看到其实质上是没有上?的,只是赋值的过程前多了一个比较的方法,因此可能引起一定的ABA问题: [狸猫换太子] 如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化。

举个栗子: 在你非常渴的情况下你发现一个盛满水的杯子,你一饮而尽。之后再给杯子里重新倒满水。然后你离开,当杯子的真正主人回来时看到杯子还是盛满水,他当然不知道是否被人喝完重新倒满。

2、UnSafe类

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

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

image.png

变量ValueOffset,便是该变量在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的

image.png

变量value和volatile修饰,保证了多线程之间的可见性

image.png

3、CAS的缺点

  • ①循环时间长开销很大
  1. 我们可以看到getAndInt方法执行时,有个do while
  2. 如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销

image.png

  • ②只能保证一个共享变量的原子性
  1. 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作
  2. 对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性

4、ABA问题的产生

(比如一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没问题的)

举个栗子: 在你非常渴的情况下你发现一个盛满水的杯子,你一饮而尽。之后再给杯子里重新倒满水。然后你离开,当杯子的真正主人回来时看到杯子还是盛满水,他当然不知道是否被人喝完重新倒满。

public class CAS_Demo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        // 我们想把数字改成6666
        // ============== 捣乱的线程 ==================
        atomicInteger.compareAndSet(2020, 2021);
        System.out.println("such a fool...");
        atomicInteger.compareAndSet(2021, 2020);

        // ============== 期望的线程 ==================
        System.out.println(atomicInteger.compareAndSet(2020, 6666));
        System.out.println(atomicInteger.get());
    }
}

复制代码

结果:

true
捣乱的线程修改值为2021
true
捣乱的线程修改值为2020
truel
期望的线程的值为: 6666
复制代码

解决上述问题的一个策略是:

  • 每一次倒水假设有一个自动记录仪记录下,也就是给操作赋值上一个标致,这样主人回来就可以分辨在他离开后是否发生过重新倒满的情况。
  • 这也是解决ABA问题目前采用的策略,这就需要我们的原子引用

5、原子引用解决ABA问题

官方说明:

一个AtomicStampedReference维护对象引用以及整数“印记”,可以原子更新。
实现注意事项:此实现通过创建表示“boxed”[引用,整数]对的内部对象来维护加盖引用。

AtomicReference是作用是对”对象”进行原子操作。 提供了一种读和写都是原子性的对象引用变量。原子意味着多个线程试图改变同一个AtomicReference(例如比较和交换操作)将不会使得AtomicReference处于不一致的状态。

AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等,而AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性。

  • ABA问题解决方案是使用 AtomicStampedReference 每修改一次都会有一个版本号
  • 注意:AtomicStampedReference用来解决AtomicInteger中的ABA问题,该demo企图将integer的值从0一直增长到1000,但当integer的值增长到128后,将停止增长。出现该现象有两点原因:
    • 使用int类型而非Integer保存当前值
    • Interger对-128~127的缓存[这个范围才有效,不在这个范围comareAndSet会一直返回false

解决:

这里用到了双参构造方法:(一个初始值,一个时间戳)

public AtomicStampedReference(V initialRef, int initialStamp) {
    pair = Pair.of(initialRef, initialStamp);
}
复制代码
public class CAS_ABA_Demo {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> intAtomicStampedReference = new AtomicStampedReference<>(1, 1);

        //A线程
        new Thread(()->{
            System.out.println("操作前stamp:A1==>"+ intAtomicStampedReference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException ignored) {}

            intAtomicStampedReference.compareAndSet(1,2,intAtomicStampedReference.getStamp(),intAtomicStampedReference.getStamp()+1);
            System.out.println("操作后stamp:A2==>"+ intAtomicStampedReference.getStamp());

            intAtomicStampedReference.compareAndSet(2,1,intAtomicStampedReference.getStamp(),intAtomicStampedReference.getStamp()+1);
            System.out.println("回滚操作stamp:A3==>"+ intAtomicStampedReference.getStamp());

            System.out.println("假装是 x="+intAtomicStampedReference.getReference());
        },"A").start();

        //B线程
        new Thread(()->{
            int stamp = intAtomicStampedReference.getStamp(); // 获得版本号
            System.out.println("操作前stamp:B=>"+ stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException ignored) {}
            System.out.println("操作是否成功:"+intAtomicStampedReference.compareAndSet(1, 6,
                    stamp, stamp + 1));
            System.out.println("操作后stamp:b2=>"+intAtomicStampedReference.getStamp());
            System.out.println("此时的值:"+intAtomicStampedReference.getReference());

        },"B").start();
    }
}

复制代码

此时就不会由于stamp,也可理解为数据库中乐观锁的版本号,时的赋值操作失败,修改不成功。

注意:

Integer 使用了对象缓存机制,默认范围是 -128 ~ 127 ,推荐使用静态工厂方法 valueOf 获取对象实
例,而不是 new,因为 valueOf 使用缓存,而 new 一定会创建新的对象分配新的内存空间;

image.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享