CAS原理

1、C A S基本概念

       C A S(compareAndSwap)也叫比较交换,是一种无锁原子算法,映射到操作系统就是一条cmpxchg硬件汇编指令(保证原子性),其作用是让C P U将内存值更新为新值,但是有个条件,内存值必须与期望值相同,并且C A S操作无需用户态与内核态切换,直接在用户态对内存进行读写操作(意味着不会阻塞/线程上下文切换)。

       它包含3个参数C A S(V,E,N),V表示待更新的内存值,E表示预期值,N表示新值,当 V值等于E值时,才会将V值更新成N值,如果V值和E值不等,不做更新,这就是一次C A S的操作。

       简单说,C A S需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的,如果变量不是你想象的那样,说明它已经被别人修改过了,你只需要重新读取,设置新期望值,再次尝试修改就好了。

2、C A S源码分析

2.1 Unsafe类

原子类中的主要组成部分:

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

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

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

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

       例如 AtomicInteger 类调用incrementAndGet()方法实现原子性的自增,内部调用Unsafe的getAndAddInt方法:

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

  在Unsafe类的getAndAddInt方法中主要是看compareAndSwapInt方法:

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;
}

  可以看到Unsafe类中的compareAndSwapInt是一个native本地方法: 

public final native boolean compareAndSwapInt(Object var1, long var2, 
                                              int var4, int var5);
  • var1:表示要操作的对象
  • var2:表示要操作对象中属性地址的偏移量
  • var4:表示需要修改数据的期望的值
  • var5:表示需要修改为的新值

 

       不难发现,cmpxchg这条汇编语言可以直接操作内存进行数据交换,实现CAS最终目的。(一条汇编指令对应一条CPU指令,是单步操作,自然是原子性的,因此谁CAS实现是硬件层面上的)

       这里看到有一个LOCK_IF_MP,作用是如果是多处理器,在指令前加上LOCK前缀,因为在单处理器中,是不会存在缓存不一致的问题的,所有线程都在一个CPU上跑,使用同一个缓存区,也就不存在缓存与主内存不一致的问题,不会造成可见性问题。

   (缓存在CPU上,主内存不在CPU上,CPU是通过缓存去读取主内存的,每个CPU对应一个缓存,不同缓存对应不同CPU,这里要结合前面的JMM模型和硬件架构理一理)

然而在多核处理器中,需要遵循缓存一致性协议通知其他处理器更新自己的缓存。

Lock在这里的作用:

  • 在cmpxchg执行期间,锁住内存地址[edx],其他处理器不能访问该内存,保证原子性。(这个就是保证CAS原子性的关键所在)
  • 写内存屏障,保证每个线程的本地空间与主存一致。
  • 禁止cmpxchg与前后任何指令重排序,防止指令重排序。

以使用AtomicInteger对变量进行自增操作为例,可以得到如下主要流程:

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

2.2 AtomicReference<V>

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

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

 AtomicReference<User> userAtomicReference=new AtomicReference<>();
        User user=new User("张三","24");
        userAtomicReference.set(user);
        User user2=new User("张三","25");
        Runnable runnable = new Runnable() {
            @Override
            public void run() {

                userAtomicReference.compareAndSet(user, user2);
                System.out.println("runnable1: "+userAtomicReference.get());
            }
        };
        Runnable runnable2 = new Runnable() {
            @Override
            public void run() {
                user2.setAge("23");
                User user3=new User("张三","27");
                System.out.println(userAtomicReference.get()==user2);
                userAtomicReference.compareAndSet(user2, user3);
                System.out.println("runnable2: "+userAtomicReference.get());
            }
        };
        runnable.run();
        runnable2.run();
        System.out.println("main: "+userAtomicReference.get());

由输出结果可知,AtomicReference可以保证对象的原子性。其中,判断对象是否一致,比较的是对象的地址,而非属性值。

3、C A S的问题 

       C A S和锁都解决了原子性问题,和锁相比没有阻塞、线程上下文你切换、死锁,所以C A S要比锁拥有更优越的性能,但是C A S同样存在缺点。C A S的问题如下:

  • 只能保证一个共享变量的原子操作
  • 自旋时间太长(建立在自旋锁的基础上)
  • ABA问题

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

        看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了,当然如果你有办法把多个变量整成一个变量,利用CAS也不错。

       例如:JDK提供的AtomicReference类来保证对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

3.2 自旋时间太长(建立在自旋锁的基础上)

       当一个线程获取锁时失败,不进行阻塞挂起,而是间隔一段时间再次尝试获取,直到成功为止,这种循环获取的机制被称为自旋锁(spinlock)。

       自旋锁好处是,持有锁的线程在短时间内释放锁,那些等待竞争锁的线程就不需进入阻塞状态(无需线程上下文切换/无需用户态与内核态切换),它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户态和内核态的切换消耗。

        自旋锁坏处显而易见,线程在长时间内持有锁,等待竞争锁的线程一直自旋,即CPU一直空转,资源浪费在毫无意义的地方,所以一般会限制自旋次数。

        最后来说自旋锁的实现,实现自旋锁可以基于C A S实现,先定义lockValue对象默认值1,1代表锁资源空闲,0代表锁资源被占用,代码如下:

      上面定义了AtomicInteger类型的lockValue变量,AtomicInteger是Java基于C A S实现的Integer原子操作类,还定义了3个函数lock、tryLock、unLock。

(1)tryLock函数-获取锁

  • 期望值1,更新值0。C A S更新如果期望值与lockValue值相等,则lockValue值更新为0,返回true,否则执行下面逻辑如果期望值与lockValue值不相等,不做任何更新,返回false。
  • 期望值0,更新值1。C A S更新如果期望值与lockValue值相等,则lockValue值更新为1,返回true,否则执行下面逻辑如果期望值与lockValue值不相等,不做任何更新,返回false。

(2)lock函数-自旋获取锁

(3)unLock函数-释放锁

       从上图可以看出,只有tryLock成功的线程(把lockValue更新为0),才会执行代码块,其他线程个tryLock自旋等待lockValue被更新成1,tryLock成功的线程执行unLock(把lockValue更新为1),自旋的线程才会tryLock成功。

3.3 ABA问题

3.3.1 什么是ABA问题:
  1. 线程1和线程2开启时,根据对线程变量的操作,把主内存的值A复制到线程中的工作内存A
  2. 线程1需要10s,线程2需要2s,假设线程2先修改则线程2中的工作内存的值A和主内存中的值A修改为B
  3. 等待线程1的过程中,线程2又把自己内存中的值和主内存中的值修改为“A”
  4. 此时线程1开启,发现线程1中的A与主内存中的“A”相同,按照CAS的方法把值修改为B

        简单的来说就是由于线程1和线程2存在时间差,线程2执行完之后又执行了一次改回来”原来的“值,线程1认为和自己的值相同,则又进行了操作。

如果cas的是个简单的数据结构,则基本上不存在问题,如果是复杂的结构,则会出现问题。列入:

 LinkedList<Integer> list = new LinkedList<>();
        list.add(2);
        list.add(3);
        AtomicReference<LinkedList<Integer>> linkedListAtomicReference=new AtomicReference<>(list);

        new Thread(()->{
            System.out.println("start runnable1: "+linkedListAtomicReference.get());
            LinkedList<Integer> linkedList = linkedListAtomicReference.get();
            linkedList.removeFirst();
            try {
                Thread. sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            linkedList.add(4);
            linkedListAtomicReference.compareAndSet(linkedList,linkedList);
            System.out.println("end runnable1: "+linkedListAtomicReference.get());
        }).start();

        new Thread(()->{
            System.out.println("start runnable2: "+linkedListAtomicReference.get());
            LinkedList<Integer> linkedList = linkedListAtomicReference.get();
            linkedList.removeFirst();
            linkedList.add(5);
            linkedListAtomicReference.compareAndSet(linkedList,linkedList);
            System.out.println("end runnable2: "+linkedListAtomicReference.get());
        }).start();
        try {
            Thread.sleep(5000);
            System.out.println(linkedListAtomicReference.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

list为2—>3

runnable1需求:去掉头,新增节点4。

runnable2需求:去掉头,新增节点5。

执行流程:

runnable1去掉头以后,睡眠。此时list为3。

runnable2拿到执行权,去掉头以后,加5,runnable2执行结束。此时list为5。

runnable拿到执行权,新增节点4。此时list为为5,4。

由于AtomicReference比较的是对象地址,runnable2操作完以后,runnable1进行cas时,判断list相等,其实内容已经改变。

3.3.2 解决ABA问题-AtomicStampedReference版本号原子引用

除去比较对象的值以外,在新增一个版本号。只有当值与版本号一致时,才认为与气质与实际值相等。

  AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
        new Thread(()->{
            int stamped = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"初始版本号为"+stamped);
            try{
                TimeUnit.SECONDS.sleep(1);
                System.out.println(atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1));
                System.out.println("第一次修改后版本号为"+atomicStampedReference.getStamp());
                System.out.println("第一次修改后当前值"+atomicStampedReference.getReference());
                System.out.println(atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1));
                System.out.println("第二次修改后版本号为"+atomicStampedReference.getStamp());
                System.out.println("第一次修改后当前值"+atomicStampedReference.getReference());
            }catch(InterruptedException e){
                e.printStackTrace();
            }

        }).start();

        new Thread(()->{
            int stamped = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"初始版本号为"+stamped);
            try{
                TimeUnit.SECONDS.sleep(2);
                System.out.println(atomicStampedReference.compareAndSet(100,2022,stamped,stamped+1));

            }catch(InterruptedException e){
                e.printStackTrace();
            }


        }).start();

  1. 线程1和线程2获取的值都为:版本号1,值100
  2. 线程1将值修改为101,版本为2。又将值修改回为100 ,版本值为3.
  3. 线程2设置值为2022时,判断预期值与内存值都为100,但是预期的版本号1和现在版本号3不等,cas失败。防止了ABA问题的发生。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
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问题以及确保程序的正确性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值