CAS原理解析、应用实战及ABA问题

JUC是java.util.concurrent包的简称,JUC有2大核心,CAS和AQS,CAS是java.util.concurrent.atomic包的基础,即AtomicInteger和AtomicLong等是用CAS实现的。

一. CAS原理

现在有一个AtomicInteger类型的变量,初始值为0。有两个线程需要同时对它进行一次自增操作,期待的结果是2。按照时间顺序分析一下两个线程具体的执行逻辑。

CAS原理分析.png

  1. t1时刻:线程1读取到当前的值是0;
  2. t2时刻:线程2也读取到当前的值是0;
  3. t3时刻:线程1先拿到CPU执行权,尝试将值设置为1,此时发现当前的值与t1时刻读取的值相等(0==0),说明没有其它线程进行改动,则将值成功设置为1;
  4. t4时刻:线程2拿到CPU执行权,尝试将值设置为1,此时发现当前的值已经变成了1(线程1所改),与t2时刻读取的值0不相等(0!=1),那么线程2此次尝试设置值失败;
  5. t5时刻:线程2重新读取当前值为1;
  6. t6时刻:线程2自增,尝试将值设置为2,此时发现当前的值还是1,与t5时刻读取的值相等(1==1),则可以成功设置值为2。
    以上的3、4、6步骤都是CAS(Compare And Swap)操作,CAS操作在底层的硬件级别保证一定是原子的,同一时间只有一个线程可以执行CAS,先比较再设置。

二.CAS 源码

/**
 * Atomically increments by one the current value.
 *
 * @return the updated value
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// 第一个参数为当前这个对象
// 第二个参数为AtomicInteger对象value成员变量在内存中的偏移量
// 第三个参数为要增加的值
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            // 调用底层方法得到value值
            var5 = this.getIntVolatile(var1, var2);
            //通过var1和var2得到底层值,var5为当前值,如果底层值=当前值,则将值设为var5+var4,并返回true,否则返回false
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

三. 利用CAS构造一个TryLock(立即失败)自定义的显示锁

背景:使用synchronized或者基于wait()的自定义的显示锁,没有抢到锁的线程将被阻塞,现在需要一种功能尝试加锁的功能,如果线程没有争抢到锁,则抛出异常,线程直接释放掉。可以用AtomicInteger来实现。

public class CompareAndSwapLock {

    /**
     * 锁标志
     * 0:锁空闲
     * 2:锁占用
     */
    private static final AtomicInteger lock = new AtomicInteger(0);

    /**
     * 记录当前占有锁的线程
     */
    private Thread lockedThread;

    /**
     * 尝试加锁
     */
    public void tryLock() throws GetLockException {

        // CAS操作,原子性的,多线程安全
        boolean success = lock.compareAndSet(0, 1);

        if (!success) {
            throw new GetLockException(Thread.currentThread().getName() +  " try lock failed");
        } else {
            lockedThread = Thread.currentThread();
        }

    }


    public void unLock() {

        if (0 == lock.get()) {
            return;
        }
        // 如果是当前线程占有锁,释放
        if (lockedThread == Thread.currentThread()) {
            lock.compareAndSet(1, 0);
        }
    }
}
public class GetLockException extends Exception {

    public GetLockException() {
        super();
    }

    public GetLockException(String message) {
        super(message);
    }
}

测试:

public class TryLockTest {

    public static void main(String[] args) {

        CompareAndSwapLock lock = new CompareAndSwapLock();

        IntStream.range(0, 3).forEach(i -> new Thread(() -> {
            try {
                // 尝试加锁
                lock.tryLock();

                try {
                    Thread.sleep(1_000);
                    System.out.println(Thread.currentThread().getName() + " do something");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } catch (GetLockException e) {
                // 尝试加锁异常,进行一些操作
                System.out.println(Thread.currentThread().getName() + " " + e.getMessage());
            } finally {
                // 释放锁
                lock.unLock();
            }

        }).start());
    }
}

测试结果:


测试结果.png

可以看出,线程0抢到了锁,正常执行,线程1和线程2没有抢到锁,抛出了异常。

四. ABA问题(带有版本号的更新)

CAS机制虽然保证了原子性,但是会引发ABA问题。何为ABA问题?假设初始值为A,线程1需要将A更新为B,但是线程2在线程1更新之前进行了两步操作,先将A更新B,再将B更新回为A,此时根据CAS原理,只要预期值与当前值相等(A=A),线程1就能成功更新为B,但是实际上其它线程已经对数据进行了两次操作,只不过经过两次操作之后数据还跟原来一样。
这种场景对某些特殊的数据结构会存在隐藏的问题,比如说栈:
[https://www.cnblogs.com/549294286/p/3766717.html]

利用AtomicStampedReference解决ABA问题:
public class ABATest {

    private static AtomicStampedReference<Integer> stampedReference
            = new AtomicStampedReference<>(100, 0);

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {

            try {
                TimeUnit.SECONDS.sleep(1);
                boolean success = stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
                System.out.println(Thread.currentThread().getName() + "尝试将100改为101,修改前版本号是:" + (stampedReference.getStamp() - 1)  + ", 修改结果:" + ":" + success);

                success = stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
                System.out.println(Thread.currentThread().getName() + "尝试将101改为100,修改前版本号是:" + (stampedReference.getStamp() - 1) + ", 修改结果:" + ":" + success);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread t2 = new Thread(() -> {

            try {
                int stamp = stampedReference.getStamp();
                System.out.println("Before sleep:stamp = " + stamp);

                TimeUnit.SECONDS.sleep(2);
                boolean success = stampedReference.compareAndSet(100, 101, stamp, stamp + 1);
                System.out.println(Thread.currentThread().getName() + "尝试将100改为101,修改前版本号是:" + stamp + ", 修改结果:" + success);

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

        t1.start();
        t2.start();
    }
}
测试结果:
测试结果.png

Thread-1期待值和当前值都为100,但是修改失败,因为stamp的期待值和当前值不相等。

AtomicStampedReference的源码
public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    return
        //期望对象的引用和版本号和目标对象的引用和版本好都一样时,才会新建一个Pair对象,然后用新建的Pair对象和原理的Pair对象做CAS操作
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

其实很多地方(ES、ZK)利用带有版本号(version)进行更新的操作就是基于该原理。

AtomicReference简介

AtomicReference类提供了一个可以原子读写的对象引用变量。 原子意味着尝试更改相同AtomicReference的多个线程(例如,使用比较和交换操作)不会使AtomicReference最终达到不一致的状态。 AtomicReference甚至有一个先进的compareAndSet()方法,它可以将引用与预期值(引用)进行比较,如果它们相等,则在AtomicReference对象内设置一个新的引用。
上面的例子如果使用AtomicReference,Thread-1能成功将100更新为101,因为它更新的时候没有stamp概念。

public class AtomicReferenceTest {

    public static void main(String[] args) {
        SimpleObject simpleObject = new SimpleObject("tom", 88);

        AtomicReference<SimpleObject> atomicReference = new AtomicReference<>(simpleObject);
        // 更新结果成功,因为期待的对象引用与当前的对象引用是同一个
        boolean success = atomicReference.compareAndSet(simpleObject, new SimpleObject("tom", 100));

        System.out.println(success);
    }

    static class SimpleObject {
        String name;
        int id;

        public SimpleObject(String name, int id) {
            this.name = name;
            this.id = id;
        }
    }
}
运行结果1.png
public class AtomicReferenceTest {

    public static void main(String[] args) {
        SimpleObject simpleObject = new SimpleObject("tom", 88);

        AtomicReference<SimpleObject> atomicReference = new AtomicReference<>(simpleObject);
        // 第一个参数传入一个新的对象,name、id与初始化的对象一样,但是更新结果任然失败,因为期待的对象引用与当前的对象引用不是同一个
        boolean success = atomicReference.compareAndSet(new SimpleObject("tom", 88), new SimpleObject("tom", 100));

        System.out.println(success);
    }

    static class SimpleObject {
        String name;
        int id;

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

推荐阅读更多精彩内容