CAS机制

1、什么是CAS?

CAS的全称是 Compare And Swap(比较再交换,确切一点称之为:比较并且相同再做交换)
是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。CAS的作用是:CAS可以将比较和交换转换为原子操作,这个原子操作直接由处理器CPU保证。

(可以看做是一个轻量级的synchronized,它能保证变量修改的原子操作)

CAS指令需要有三个操作数,分别是:

  • 内存位置(在Java中可以简单地理解为变量的内存地址,用V表示)
  • 旧的预取值(用A表示)
  • 准备设置的新值(用B表示)
    在这里插入图片描述
    CAS指令执行时,当且仅当 V 符合 A 时,处理器才会用 B 更新 V 的值,否则它就不执行更新 或 重来(当他重来重试的这种行为称为——自旋)。但是不管是否更新了 V 的值,都会返回 V 的旧值。该过程是一个原子操作,执行期间不会被其他线程中断。

它是一种CPU并发原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

说了这么多原理,撸一下Demo吧~通过实现类AtomicInteger来演示一下CAS:

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(6);

        System.out.println(atomicInteger.compareAndSet(6, 2022) + "\t" + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(6, 2022) + "\t" + atomicInteger.get());
    }
}

在这里插入图片描述

第一次C的值等于A,故将C改为B。第二次C得值不等于A,故不修改。

2、CAS实现原子操作的3大问题?

ABA问题、循环时间长消耗资源大、只能保证一个 共享变量的原子操作。

循环时间长消耗资源大

比如说源码 getAndAddInt方法执行时,有个 do while。如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

ABA 问题

​ 如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然为 A 值,那就能说明它的值没有被其他线程改变过了吗?

​ 这是不可能的,因为 如果在这段期间它的值曾经被改成 B,后来又被改回为 A,那CAS操作就会误人误它从来没有被改变过。这个漏洞称为 CAS 操作的 “ABA问题”。接下来个Demo~

public class ABADemo {
    static AtomicInteger atomicInteger = new AtomicInteger(100);
    public static void main(String[] args) {
       new Thread(()->{
           atomicInteger.compareAndSet(100,200);
           // 暂停10毫秒
           try {
               TimeUnit.MILLISECONDS.sleep(10);
           } catch (InterruptedException e) {
               e.printStackTrace();
           } finally {
               atomicInteger.compareAndSet(200,100);
           }
       },"t1").start();

        new Thread(()->{
            // 暂停200毫秒
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(atomicInteger.compareAndSet(100,500) + "\t" + atomicInteger.get());
            }
        },"tw").start();
    }
}

代码中 t1线程将值修改为200,但又在t2线程读取之前修改为100。但t1线程不知道呀,就会误人误它从来没有被改变过。

那么如何解决ABA问题呢?可以使用java.util.concurrent.atomic.AtomicStampedReference<V>类来解决

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

    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
    public static void main(String[] args) {
        new Thread(()->{
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t"+"首次版本号:"+stamp);
            // 暂停500毫秒,保证后面的t2线程初始化拿到的版本号和t1一样
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stampedReference.compareAndSet(100, 200, stampedReference.getStamp(), stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t"+"2次版本号:"+stampedReference.getStamp());
            stampedReference.compareAndSet(200,100,stampedReference.getStamp(), stampedReference.getStamp()+1);

        },"t1").start();
        new Thread(()->{
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t"+"首次版本号:"+stamp);
            // 暂停1000毫秒,保证t2线程初始化拿到的版本号和t1一样
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = stampedReference.compareAndSet(100, 500, stamp, stampedReference.getStamp() + 1);
            System.out.println(b + "\t" + stampedReference.getReference() + "\t" + stampedReference.getStamp());
        },"t2").start();
    }
}

3、Unsafe类

从 JDK5 之后,Java类库中才开始使用CAS操作,该操作由 sun.misc.Unsafe 类里面的 compareAndSwapXXX()方法包底层实现即为CPU指令cmpxchg。

执行 cmpxchg 指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功会执行CAS操作,也就是说CAS的原子性实际上是CPU实现独占的。

Unsafe类详解

在这里插入图片描述

1、Unsafe

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

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

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

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

源码解读

接下来我们来分析一下源代码:

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

通过点进compareAndSet() 方法的源代码,发现其是调用了unsafe.compareAndSwapInt()方法,在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:表示需要修改为的新值

这里对偏移量做一下讲解,大学汇编里有说过~this 相当于当前对象的首地址,需要找到对应的value在内存中的存放位置,此时就需要一个偏移量,即:收地址+偏移量=值在内存中的位置

我们已知i++在多线程情况下是不安全的,那 atomicInteger.getAndIncrement() 方法呢?

在这里插入图片描述

假设线程A和现场B两个线程同时执行 getAndIncrement()方法(分别跑在不同CPU上):

  1. 假设主内存中 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 方法比较,发现主内存中的值和旧的预期值不一致,说明该值已经被其他线程更新了,则线程A本次修改失败,自旋重来一次。
  5. 线程A重新获取value值,因为变量value被volatile修饰,所以其他线程对它的修改,线程A是可见的,线程A继续执行 compareAndSwapInt 进行比较替换,直到成功为止。

Unsafe类中的compareAndSwapInt,对应着本地方法,该方法的实现位于unsafe.cpp,让我们一探究竟~

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xilwh0X8-1656433185456)(JUC并发编程.assets/image-20220628234302944.png)]

4、AtomicReference<V>

  1. AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,而AtomicReference则对应普通的对象引用。也就是它可以保证你在修改对象引用时的线程安全性。

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

即 可以原子更新的对象引用。

首先编写一个User类:

class User{
    String username;
    int age;
		// 省略全参构造方法、setter、getter、toString
}

通过AtomicReference类实现对”User对象”进行原子操作。

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        AtomicReference<User> userAtomicReference = new AtomicReference<>();
        User hgw = new User("hgw", 22);
        User hly = new User("hly", 22);
        userAtomicReference.set(hly);
        System.out.println(userAtomicReference.compareAndSet(hly, hgw) +"\t,"+ userAtomicReference.get());
        System.out.println(userAtomicReference.compareAndSet(hly, hgw) +"\t,"+ userAtomicReference.get());
    }
}

5、CAS——自旋锁

谈CAS的话当然得谈谈自旋锁啦,这边手写个自旋锁给大家演示一下吧~

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

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

        }
        System.out.println(Thread.currentThread().getName()+"\t"+"lock");
    }

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

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(()->{
            spinLockDemo.lock();
            // 暂停几秒钟线程
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                spinLockDemo.unLock();
            }
        },"A").start();

        new Thread(()->{
            spinLockDemo.lock();
            spinLockDemo.unLock();
        },"B").start();
    }
}

在这里插入图片描述

  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值