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

你可能感兴趣的:(CAS原理解析、应用实战及ABA问题)