CAS算法和ABA问题

CAS算法

CAS(Compare And Swap)比较并交换,它是一种算法,体现的是乐观锁的思想,总是认为自己可以成功完成操作,在多个线程同时使用CAS操作一个变量时,只有一个会胜出并成功更新,其余均会失败,失败的线程不会被挂起,仅被告知失败,并且允许再次尝试。

三个重要参数:

  • V:当前内存中的值
  • A:旧的预期值
  • B:要更新的值

当且仅当 V == A 时,通过原子操作将 V 修改为 B,并返回 true,否则什么都不做,返回 false

CAS算法必须和 volatile 一起使用,并且要在多核CPU下才可以实现。

使用CAS算法的类有JUC包下的AtomicInteger类,其getAndSet()就是调用Unsafe类的getAndAddInt()

AtomicInteger类的部分源码如下:

public class AtomicInteger extends Number implements java.io.Serializable {
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    
    private static final long valueOffset;
    static {
        try {
            //表示该变量值在内存中的偏移地址
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    
    //线程都会直接读取value并且不缓存它,保证value在多线程之间的内存可见性
    private volatile int value;
    public final int get() {return value;}
    
    public final int getAndAdd(int delta) {    
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
}

Unsafe类的部分源码如下:

public final class Unsafe {
    
    private Unsafe() {}

    private static final Unsafe theUnsafe = new Unsafe();
    
    // 拿到调用它的那个Class的ClassLoader判断是否是SystemDomainLoader,如果不是则抛安全异常
    // 就是判断是否可以信任调用者并返回他实例
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }
    
    
   /**
     * Object obeject 指定要获取哪个对象中的属性
     * long valueOffset 指定该属性在内存中的偏移值
     * int i 加数
     * 知道 obeject 和 valueOffset 才能确定共享变量
     * 源码中的变量为var1 var2,不能闻其名知其义,所以我改了名字,方便阅读
     */
    public final int getAndAddInt(Object obeject, long valueOffset, int i) {
        int A;
        do {    
            // 获取当前内存中的值,即旧的预期值
            A = this.getIntVolatile(obeject,valueOffset);
            
            // 这里的obeject, valueOffset传递给底层去确定 V(当前内存中的值)
        } while (!compareAndSwapInt(obeject, valueOffset, A, A + i));
        
        return A;
    }
    
}

.

ABA问题

CAS算法的实现有一个前提:需要取出内存中某时刻的数据,然后在下一时刻进行比较和交换,在这个时间差内可能数据已经发生了改变,就会产生ABA问题。

ABA问题指第一个线程从内存的V位置取出A,这时第二个线程也从内存的V位置取出A,并将V位置的数据修改为B,接着又将V位置的数据修改为A,然后第一个线程操作成功。对于第一个线程来说,CAS操作是成功的,但是该过程中V位置的数据发生了变化,第一个线程感知不到,在某些应用场景下可能出现过程数据不一致的问题。

解决办法:

每次对共享变量进行修改操作时都会带上一个版本号,在预期的版本号和数据的版本号一致时就可以执行修改操作,并对版本号执行加1操作,否则执行失败。因为每次操作都会让版本号进行加1,所以不会出现ABA问题,版本号只能增加不能减少。

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。

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)));
    }
    
    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }
}

.

CAS缺点

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