并发编程-CAS

  • Post author:
  • Post category:其他




说在前面的话

正如我开篇所说,我要整理一些java并发编程的学习文档,这一篇就是第二篇:java中的CAS。

这一篇主要说的CAS的原理,以及java中的CAS的一些应用吧!

欢迎关注和点赞。



开整



CAS是个啥

CAS,compare and swap的缩写,中文翻译成比较并交换。


首先想说的就是:CAS只是一个思想,原理。 本身没有问题,也没有所谓的ABA问题,也没有所谓的乐观锁长时间占用资源的问题。这些问题都是因为我们使用CAS的方式导致的。 不信?看我细细道来…

CAS描述起来大概是: 准备一个预期值和新值,拿预期值和要修改的变量的空间中已有的值进行比较,如果一样,就使用新值替换原始值,否则就算了。 大致就是下面这个图:

在这里插入图片描述

图很丑,我再描述以下,使用预期值A和内存值X进行比较,如果相同,就使用新值B替换内存值X。 不相同就算了。 这就是所谓的比较并交换。

CAS可是NB的不行的原理和技术。java中的整个JUC包中的内容基本都是基于CAS的。

CAS为啥NB呢,因为CAS是基于CPU级别的指令执行的,那是真的快。

我来一段java程序模拟一个看看:

/**
 * @author 戴着假发的程序员
 */
public class CASTest {
    // 要修改的变量
    private static int x  = 5;
    public static void main(String[] args) {
        boolean cas = cas(5, 50);
        System.out.println(cas); // true
    }

    /**
     *  CAS 的模拟操作
     * @param v 预期值
     * @param b 新值
     * @return
     */
    public synchronized static boolean cas(int v,int b){
        if(v == x){
            x = b;
            return true;
        }
        return false;
    }
}

这个程序看上去应该很好理解。 CAS中比较v和x的值,如果相同,就把b的值赋值给x,否则就什么都不做。



CAS和ABA问题

ABA问题在上一小节中有所提及,这里再次认真的分析一边。


实际上ABA问题并不是CAS的问题

,CAS原理本身是没有问题的。ABA是因为一种操作造成的。

大致是这个意思:

(请理解一个头发被程序夺走的程序的美术功底。。。图很乱,看下面文字说明)

在这里插入图片描述

有个树洞(内存空间),里面有个数字3。

小黑先使用克隆机器从树洞中把数字3克隆一份出来,并且将克隆出来的3作为预期值,准备使用CAS将树洞里面的3更换为7。但是就是在这个时候,小黑女朋友找他,他就走神了。

此时小蓝来了,小蓝也是先把树洞里面的3克隆一份出来,将克隆的3作为预期值,并且使用CAS将树洞里面的3修改为5,但是立马发现有点问题,又立刻使用CAS把树洞里面的5替换回3。一套操作行如流水。

这时小黑那边女朋友的事情处理完了,才回来执行CAS准备将3替换为7。首先使用自己手中的3和树洞里面的3进行比较,发现一样(当然其实我们知道次3非彼3),于是就使用7替换了树洞里面的3。

这里的问题很明显,就是小黑取出内存的值之后,才进行比较就是这个时间差,树洞里面的值有可能已经被来回的替换了多次,最后正好又给换回了3,但是这个3已经不是原来的3了。 这就是ABA问题。

打字真累,看程序理解一下:

import java.util.concurrent.TimeUnit;

/**
 * @author 戴着假发的程序员
 */
public class CASTest1 {
    // 要修改的变量
    private static int x  = 3;
    public static void main(String[] args) {
        new Thread(()->{
            System.out.println("小黑开始");
            int a = x;// 取出x作为预期值
            System.out.println("小黑被睡了....");
            try {
                TimeUnit.MICROSECONDS.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(a == x){ //判断预期值和x是否相同
                x = 7; // 使用新值替换x的值
            }
            System.out.println("小黑替换后的值:"+x);
        }).start();
        new Thread(()->{
            System.out.println("小蓝开始了");
            int a = x;// 取出x作为预期值
            if(a == x){ //判断预期值和x是否相同
                x = 5; // 使用新值替换x的值
            }
            System.out.println("小蓝第一次替换后的值:"+x);
            a = x; // 立刻换回去
            if(a == x){
                x = 3;
            }
            System.out.println("小蓝第二次替换后的值:"+x);
        }).start();
    }
}

在这里插入图片描述

嗯…结果很明显了。

问题就是这么个问题,怎么解决呢?你要问怎么解决?

嗯…首先再说一遍,ABA是使用CAS的操作的问题,跟CAS本身无关,你只要不按照上面的方式操作就不会有ABA问题呀。

比如:预期值不是从变量x中获取的。也就不会有取值和比较的时间差的问题。自然就不会有ABA文件。当然在实际的使用中,我们很多时候都是上面的操作模式。



CAS都在哪里用了

CAS在JCU包中的很多类中都有用到。比如我们上一小节说的Atomic系列中。

哪到低是咋用的呢?

我们来翻翻源码,look look。

先来看看:AtomicInteger的compareAndSet方法

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

用了Unsafe类中的compareAndSwapInt方法。

再来看看 AbstractQueuedSynchronizer 类的 compareAndSetState()方法。

啥?AbstractQueuedSynchronizer是啥? AbstractQueuedSynchronizer。 你看这个单词跟AQS像不像?后面会有AQS的详解的文章。 欢迎关注。。。。

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

这里还是Unsafe类中的compareAndSwapInt方法。

好吧,来看看Unsafe类中的compareAndSwapInt方法:

额 。。。。。 此时此刻,我赋值这样的代码了:(看我的注释说明吧)

对了,还要提醒一句,Unsafe是直接操作内存的。我后面的文章也会有对Unsafe类的专门讲解,欢迎关注。

package sun.misc;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.security.ProtectionDomain;
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;

public final class Unsafe {
    //..........
/**
*@param obj 要操作的对象
*@param offset 要改变的变量,也就是对象的这个属性,相对于这个对象的对象头而言的位置偏移量
*@param expected 预期值
*@param x 新值
*/
public final native boolean compareAndSwapInt(Object obj, long offset, int expected, int x);
    //........
}

OK!此时我们也清楚了,JUC中所有用到的CAS都是Unsafe类中的CAS相关的方法。

至于都用到哪里了…嗯…


首先

就是Atomic系列的类中的大部分方法都用到了,只要需要更新内存的值的方法都用到了。这个就不用多说了。


还有

就是我们常说的乐观锁,比如synchronized的轻量级锁,还有就是ReentrantLock系列。还有部分同步集合类(这些类后面也会有讲的,欢迎关注呦。。。。)

关于在锁中的用法大致是这样的:

​ 我们要知道所谓锁,就好比高铁上的厕所使用标识一样。在资源前面设置一个标识,当资源被占用(被锁)的时候这个标识是一个值,当资源空闲的时候,这个标识是另外一个值,任何一个线程要使用这个资源都要获取锁,所谓获取锁就是尝试将这个标识修改为被占用的状态,谁修改成功谁就可以使用这个资源。 所谓乐观锁就是使用循环不断的尝试使用CAS修改这个锁标识(大致就是你在上厕所的时候,外面有个人一直敲门,问你好了没…)。 OK这就是CAS在锁中的大致用法。



总结

​ 最后总结以下,CAS本身其实很好理解,不需要长篇大论。但是还是写了这么多字,那是因为CAS在并发编程包中用的太多了。

CAS原理和技术,本身没有问题,CPU级别的指令,效率贼高。但是在使用过程中,由于我们的需求,可能会产生一些问题。这些问题,有的需要解决,有的不需要解决。无论如何我们要理解CAS,了解使用中的问题,只有这样才能必坑。

​ 这就是,之前有同学问我,我毕业了就是写写CRUD,面试官为啥要问我这么多的高并发的问题。我的回答就是:你写程序的时候,就奔着一个目标,实现需求。 而我还会考虑另外一个问题,在高并发情况下,在分布式情况下我写的程序会不会有并发问题。 这就是此刻,你不需要解决高并发的问题,但是你的程序要准备好接受高并发的考验。

好了,这篇,就到这里了。如果觉得还不错,记得点赞。 欢迎关注…你的肯定,是我更新的动力。



版权声明:本文为q2780004063原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。