【Java并发编程】CAS原理分析

概述

上一篇文章Java锁分类中,有提到一种分类的思想:乐观锁和悲观锁。悲观锁,总认为每次访问共享资源的时候,都有可能发生资源竞争,所以在线程获取到共享资源后,需要加锁,其余线程无法再对共享资源进行操作。而乐观锁,总认为每次访问共享资源时,都不会发生资源竞争,线程可以不停的执行,无需加锁,无需等待;当发生冲突的时候,会采用CAS来保证线程执行的安全性。

CAS全称Compare And Swap,即比较和交换,该技术是实现用于多线程中原子指令。其算法核心思想如下:CAS(V,A,B),其中V代表内存中的值,A为预期值,B为改变后的新值。

CAS原理

我们知道在多线程环境下,有些共享资源存储在主内存中,每个线程对共享资源都有自己的一份线程副本,当“线程1”在线程本地副本中经过一系列操作后,将比较值A1修改为新值B1后,需要更新主内存中的V值。这时会拿V值与A1值进行比较,如果相等,则说明V值在"线程1"执行期间没有被修改,那么将主存中的V值修改为B1。

假如“线程2”与“线程1”同时拿到共想资源的值,线程2执行操作较慢,在线程2本地内存副本中经过操作,将A2值修改为B2值,然后去更新主存中的值,这时候发现V与预期值A2不想等,那么说明V值在“线程2”执行期间被修改。那么“线程2”可以选择放弃执行,或者会重新读取新的V值到本地线程副本中,重新执行操作,再重新判断是否更新V值。以下图为例:

CAS原理.png

基于这样的原理,CAS操作即使没有加锁,一样能够知道其他线程对共享资源是否产生影响,并做出相应的措施。同时从这点也可以看出,由于无锁操作中没有锁的存在,所以不可能出现死锁的情况。

说到这里,我们可能会产生这样的疑问:会不会存在“线程1”的A1值与主存V比较时相等,但是在将V更新为B1的时候,V值却被其余线程修改了呢?答案是否定的,因为CAS是一种系统原语,原语属于操作系统用语范畴,是有若干条指令组成的,用语完成某个功能的一个过程,并且系统原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是CPU的原子性操作,不会造成数据不一致问题。

UnSafe类

Java中CAS的实现依赖于Unsafe类的方法,Unsafe类的内部方法可以像C语言一样直接操作系统内存,其所有方法都是native关键字修饰,单从名称看,该类就是不安全的,毕竟Unsafe类拥有像C一样操作内存的方法,所以我们不应该优先使用Unsafe类,Java官方也不建议使用该类。

下面列举一下CAS相关的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);

上述三个方法入参基本相同,第一个参数var1为给定的Object对象,第二个参数var2为对象内存的偏移量,通过该参数可以快速定位到字段并设置或获取字段的值。第三个参数var4为预期值,最后一个参数为要设置的值,这三个方法都通过cas原子指令执行操作。

Atomic系列中CAS的使用

在java.util.concurrent.atomic包下,有一系列的atomic类,提供了很多基于CAS实现的原子操作类,用法方便,性能高效。其中原子操作更新基本类型的主要包括以下三类:

  • AtomicInteger
  • AtomicBoolean
  • AtomicLong

我们以AtomicInteger为例,来分析以下源码

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

    //初始化指针类Unsafe
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //变量“value”在AtomicInteger实例对象内的内存偏移量
    private static final long valueOffset;

    static {
        try {
            //通过unsafe.objectFiledOffset获取到value的内存偏移量,通过偏移量,可以获取到value进行赋值或取值操作
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    
    private volatile int value;
   
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    public AtomicInteger() {
    }
    //获取最新的value值
    public final int get() {
        return value;
    }
    //设置当前值,具备volatile效果,用final修饰是为了进一步保证线程安全
    public final void set(int newValue) {
        value = newValue;
    }
    //最终会设置为newValue,但是有一定延迟,使用该方法后可能导致其他线程在之后的一小段时间内获取到旧的值,类似于延迟加载
    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }
    //设置新值获取旧值,底层使用CAS操作unsafe.compareAndSwapInt
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
    //如果当前value值等于expect预期值,那么将value更新为update值
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    //当前值加1,返回旧值
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    //当前值减1,返回旧值
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }
    //当前值加delta,返回旧值
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    ......

}

分析源码可以看出,AtomicInteger所有方法,底层都会调用Unsafe的相关方法,而所有关于值的自增和自减都间接调用了unsafe.getAndAddInt方法,该方法源码如下:

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

可以看到其通过while循环,来调用底层CAS方法compareAndSwapInt,直到值修改完成,然后返回旧值。

CAS的ABA问题

我们回到之前的场景中,假设线程1将主存中的V=0值修改为2,然后在线程2修改V值之前,有一个线程3拿到本地线程副本V值为2,然后通过运算将主存中的V值又修改为0了。那么在线程2再去修改的时候,它是没法感知主存中的V值是被修改过的,这就是经典的ABA问题。可能有的人会有疑问说,这样的操作逻辑上是没有问题的啊,最终得到的结果是线程2成功修改主存的值,可是如果我们应用到实际场景中,就会发现问题所在了。

例如在银行场景中,某公司账户有100万存款,某职员通过某种操作,挪用了公款100万去炒股,在公司查账之前,又将100万还了回来,如果这种ABA问题不得已解决,那么公司查账也就无法感知到该职员有挪用过公款了,而这种情况在现实生活中是不被允许的。

可以通过下列图示来理解ABA问题

CAS的ABA问题.png

Java也提供了两个类来解决ABA问题,分别是:AtomicStampedReference、AtomicMarkableReference。他们解决的方法类似,都是给值加上标记,只是一个是时间戳,一个是boolean值。

AtomicStampedReference

public class AtomicStampedReference<V> {
    
    //保存实例对象信息的内部类
    private static class Pair<T> {
        //实例对象
        final T reference;
        //时间戳
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;
    
    //expectedReference为预期值,newReference为新值,expectedStamp为预期时间戳,newStamp为新的时间戳
    //只有当预期值与当前值相等,并且预期时间戳与当前的时间戳相等时才会修改值
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

    .......
}

通过分析源码,我们可以看到,AtomicStampedReference通过Pair私有内部类存储数据和时间戳, 并构造volatile修饰的私有实例,接着看AtomicStampedReference类的compareAndSet()方法的实现:同时对当前数据和当前时间进行比较,只有两者都相等是才会执行casPair()方法,单从该方法的名称就可知是一个CAS方法,最终调用的还是Unsafe类中的compareAndSwapObject方法。到这我们就很清晰AtomicStampedReference的内部实现思想了,只有两者都符合预期才会调用Unsafe的compareAndSwapObject方法执行数值和时间戳替换,也就避免了ABA的问题。

AtomicMarkableReference

该类的解决方案与AtomicStampedReference类似,只是AtomicMarkableReference维护的是boolean标识,也就是维护true和false的切换,但是也很容易想到因为只有两种状态的切换,所以也没法完全避免ABA问题的发生。下面简单看一下相关源码

public class AtomicMarkableReference<V> {

    private static class Pair<T> {
        final T reference;
        final boolean mark;
        private Pair(T reference, boolean mark) {
            this.reference = reference;
            this.mark = mark;
        }
        static <T> Pair<T> of(T reference, boolean mark) {
            return new Pair<T>(reference, mark);
        }
    }

    private volatile Pair<V> pair;

    public boolean compareAndSet(V       expectedReference,
                                     V       newReference,
                                     boolean expectedMark,
                                     boolean newMark) {
            Pair<V> current = pair;
            return
                expectedReference == current.reference &&
                expectedMark == current.mark &&
                ((newReference == current.reference &&
                  newMark == current.mark) ||
                 casPair(current, Pair.of(newReference, newMark)));
        }

        ......
    }

注:本文部分参考一下博文
https://blog.csdn.net/qq_22771739/article/details/82824353
https://www.cnblogs.com/javalyy/p/8882172.html
https://www.jianshu.com/p/ae25eb3cfb5d

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,406评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,976评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,302评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,366评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,372评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,457评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,872评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,521评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,717评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,523评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,590评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,299评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,859评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,883评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,127评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,760评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,290评论 2 342

推荐阅读更多精彩内容