Java CAS原理分析

什么是CAS?

   CAS(Compare and Swap)比较并交换。里面涉及到unSave类和自旋锁。

   CAS是一条CPU并发原语,它的功能是判断内存中某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

public class CASTest {
    public static void main(String[] args) {

        //新建一个原子整型实例,初始值设置为6,不设置则默认为0
        AtomicInteger integer = new AtomicInteger(6);
        System.out.println(integer.compareAndSet(6, 8)+"\t 当前值="+integer.get());
        System.out.println(integer.compareAndSet(6, 66)+"\t 当前值="+integer.get());
    }
}

--- - - - - - - - - - - --- - - - - -
true	 当前值=8
false	 当前值=8

   先比较后交换   核心方法就是上面代码中的 integer.compareAndSet(6, 8);第一个参数代表期望值,第二个参数表示最后更新成的数值。上面代码在第一次打印的时候,期望值是6,更新值是8,由于该原子整型在声明时设置的初始值就是6,所以先比较,发现初始值和期望值是相等的,然后交换该值为8;但在第二次打印时,此时内存中的数值已经被更新成8,这时再比较发现8和期望值6不相等,所以无法更新,最后的值还是8。

       上图中线程B更新失败,此时线程B只能重新从主内存中拷贝最新的变量副本,重新赋值,重新写回主内存,如果依然比其他线程慢一拍,导致比较不相等,则循环以上操作,直到比较成功相等为止。

CAS底层原理

   自旋锁+UnSafe类

   这里引出上篇文章中 ( Volatile详解 ) 提到的问题:为什么“加个Atomic”就能保证原子性呢?看AtomicInteger源码先:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    //CAS之所以能保证原子性,靠的就是这里的UnSafe类
    //UnSafe类路径:C:\Program Files\Java\jdk1.8.0_162\jre\lib\rt\sun\misc下
    private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();

    //这个VALUE是内存地址偏移量,这样Unsafe类就可以不用知道该地址的变量值
    //只需要知道地址的偏移量就可以通过C的指针来操作数据
    private static final long VALUE;

    static {
        try {
            VALUE = U.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }

    //这里的value便是AtomicInteger中的变量,用volatile修饰,保证其可见性
    private volatile int value; 
...
...
...

}

Unsafe类是CAS的核心类,类中所有的方法都是native修饰的,所以Unsafe类中的方法都直接调用操作系统底层资源执行相应的任务。由于Java方法无法直接访问底层系统,需要通过本地(native方法)来访问,基于Unsafe类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C语言的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

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

  接着尝试深挖一下Unsafe类的源码,如下:

AtomicInteger integer = new AtomicInteger(6);
integer.getAndIncrement();

---点进 getAndIncrement(); 方法---

public final int getAndIncrement() {

    //U表示Unsafe类的实例,this表示AtomicInteger类,VALUE表示变量地址偏移量,1就是最后要加的1.
    return U.getAndAddInt(this, VALUE, 1);
}

---点进 getAndAddInt 方法---

public final int getAndAddInt(java.lang.Object o,long l,int i) {/* compiled code */}

---getAndAddInt 底层源码是---

//unsafe.getAndAddInt
public final int getAndAddInt(Object var1,long var2,int var4){
    int var5;
    do{
        //这里var1是当前对象AtomicInteger ,var2是地址偏移量VALUE
        //通过这俩参数获取该地址的真实值var5
        var5 = this.getIntVolatile(var1,var2);
    } while(!this.compareAndSwapInt(var1,var2,var5,var5 + var4));
        return var5;
}

可以看到,源码最后用了一个do...while循环,大致步骤为:

  1. 先通过var1和var2这两个参数获取主内存中的真实值var5;
  2. 用该对象var1的当前值与var5比较,若相等,则加var4(即1)并返回;
  3. 若不相等,则继续循环1、2步骤,直到相等。

以上是我个人理解,下面请看详细步骤:

      假设有线程A和B,同时执行getAndAddInt方法

  1. AtomicInteger初始值为6,即主内存中的值为6,根据JMM模型,线程A和B的工作内存中都有主内存变量值6的拷贝副本
  2. 线程A通过 getIntVolatile 方法得到值6,假设此时A突然被挂起(线程的执行和挂起由CPU调度控制)
  3. 线程B也通过 getIntVolatile 方法得到值6,幸运的是B没有被挂起,执行 compareAndSwapInt 方法比较主内存值也是6,比较成功并 6 + 1 = 7,最后将7写回主内存,此时主内存值变为7,线程B打完收工
  4. 此时线程A恢复执行,通过 compareAndSwapInt 比较发现内存中的值是 7 不是 6 ,说明自己来晚了一步,主内存的值被其他人动过了,比较失败只能重新获取最新值再进行比较
  5. 线程A重新获取值 7 ,由于变量被volatile修饰,所以任何线程对变量的修改,其他线程都能看得到,线程A继续执行compareAndSwapInt 方法进行比较替换,直到成功

compareAndSwapInt 方法内部代码:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env,jobject unsafe,jobject obj,jlong offset,jint e,jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt");

 opp p = JNIHandles::resolve(obj);
 //想办法拿到变量的内存地址
 jint* addr = (jint*)index_opp_from_field_offset_long(p,offset);
 //比较替换,x是即将更新的值,e是原内存的值
 return (jint)(Atomic::cmpxchg(x,addr,e)) == e;
UNSAFE_END

为什么要用CAS而不是用synchronized?

因为synchronized会加锁,同一时间段只允许一个线程访问,一致性得到了保障,但是并发性下降;

而CAS没有用锁,而是通过循环的方式进行CAS比较,直到比较成功为止,既保证了一致性,又不影响并发性。

CAS的缺点

  1. 如果CAS长时间比较失败,CPU开销会比较大
  2. 对于多个共享变量的操作,CAS就无法保证其原子性,此时可以用锁来代替
  3. 引发ABA问题

什么是ABA问题?

       CAS算法实现的一个重要前提是需要取出内存中某一时刻的数据并在当下时刻进行比较并替换,那么在这个时间差内有可能数据已经变化了多次。倘若在这个时间差内,数据改变了多次并最终又变回最初的数值,而其他并行处理的线程并无察觉数据的改变,依然可以通过CAS法则进行比较替换,但这并不代表没有问题。——此为ABA问题的来源

      假如有两个线程1和2同时操作一个变量value,value初始值为A,线程1处理变量的时间为5s,线程2处理变量的时间为2s,这种情况下,肯定线程2会强先一步完成value的操作,假如线程2在第2秒将value改位了B并写回主内存,又在第四秒将B又改回了A并写入了主内存,接着在第5秒,线程1执行完毕要将它的最新值写回主内存,在经过CAS比较时,发现主内存是A,跟自己想要的一样,但此时的A已经不是原来那个A,是已经被线程2操作了两次改成的A。虽然线程2也能顺利完成比较并替换,但这是有问题的,也就是ABA问题。

如何避免ABA问题?

    在此之前,先要了解什么是“原子引用”

    举个栗子 ↓?↓

//首先定义个User类
class User {

    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class ABADemo {
    public static void main(String[] args) {

        User z3 = new User("张三",30);
        User l4 = new User("李四",40);
        User w5 = new User("王五",50);
        
        //此为原子引用类,此时引用的是User,初始值设置为:张三
        AtomicReference<User> atomicReference = new AtomicReference<>(z3);

        System.out.println(atomicReference.compareAndSet(z3, l4)+"\t 当前人="+atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(z3, w5)+"\t 当前人="+atomicReference.get().toString());
    }
}

---打印结果-— - - -  -- - - - - - - -  - - -

true	 当前人=User{name='李四', age=40}
false	 当前人=User{name='李四', age=40}

话不多说,原理跟上面的AtomicInteger一模一样。  

吃完这个栗子↑?↑,我们再来YY如何避免ABA问题——给变量附加一个版本号(类似于时间戳)

 这里再引入一个概念:AtomicStampedReference——有邮戳的原子引用

/**
 *这里举一个最简单的栗子
 */
public class ABATest {
    public static void main(String[] args) {

        //初始化一个有邮戳的原子引用,并设置引用的初始值和初始版本号为A和0
        AtomicStampedReference<Character> atomicStampedReference = new AtomicStampedReference<>('A',0);

        System.out.println(atomicStampedReference.compareAndSet('A', 'B',0,1)
                +"\t 当前值="+atomicStampedReference.getReference()+"\t 当前版本号="+atomicStampedReference.getStamp());
        System.out.println(atomicStampedReference.compareAndSet('B', 'A',1,2)
                +"\t 当前值="+atomicStampedReference.getReference()+"\t 当前版本号="+atomicStampedReference.getStamp());
        //以上两步已经形成了一个ABA问题,此时下面的第三步操作由于版本号不是我们想要的0而比较失败
        System.out.println(atomicStampedReference.compareAndSet('A', 'C',0,2)
                +"\t 当前值="+atomicStampedReference.getReference()+"\t 当前版本号="+atomicStampedReference.getStamp());
    }
}

- - 打印结果- - - - - - - - - -  - - - - - - - 

true	 当前值=B	 当前版本号=1
true	 当前值=A	 当前版本号=2
false	 当前值=A	 当前版本号=2

  AtomicInteger可以引发一系列的知识点,如:

  CAS------>Unsafe------>CAS底层原理------>ABA问题------>原子引用更新------>如何规避ABA问题

  理清思路,便于理解和记忆

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java CAS(Compare And Swap,比较并交换)是一种常用于多线程编程的原子操作。其主要作用是在多线程环境下保证变量的原子性和一致性,解决多线程竞争条件下的并发问题。 Java CAS原理是通过比较内存中的值与期望值是否相等来确定是否进行交换,其核心思想是利用硬件的原子性操作来实现并发的同步,而不需要使用锁(synchronized)等机制。 具体来说,CAS操作包含三个参数:内存地址、旧的预期值和新的值。 1. 首先,CAS会将当前内存地址中的值与旧的预期值进行比较,如果相等,则说明内存中的值未被其他线程修改。 2. 然后,CAS会使用新的值来更新内存地址中的值,完成交换操作。 3. 最后,CAS会返回旧的预期值,可以通过返回值进行判断操作是否成功。 需要注意的是,CAS是一种乐观的并发控制方式,它不会阻塞线程,而是通过不断重试的方式来保证操作的原子性。如果CAS操作失败,那么线程会重新读取内存中的值,并重新尝试进行CAS操作,直到成功为止。 然而,CAS也存在一些问题。首先,CAS需要频繁地读取和写入内存,这对内存带宽的要求较高;其次,由于CAS操作是无锁的,因此存在ABA问题,即在操作过程中,如果其他线程修改了预期值两次并恢复为原来的值,CAS操作无法察觉。 总之,Java CAS作为一种基于硬件支持的原子操作,可以在多线程环境下实现高效的同步控制,然而它也需要开发人员自行处理ABA问题以及确保程序的正确性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值