CAS与ABA问题的解决

声明:本文为作者原创,如若转发,请指明转发地址

1、CAS是什么?

interface Account {
    // 获取余额
    Integer getBalance();

    // 取款
    void withdraw(Integer amount);

    /**
     * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
     * 如果初始余额为 10000 那么正确的结果应当是 0
     */
    static void demo(Account account) {
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(10);
            }));
        }
        long start = System.nanoTime();
        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(account.getBalance()
                + " cost: " + (end - start) / 1000_000 + " ms");
    }
}

//下面的方法时线程不安全的
class AccountUnsafe implements Account {

    private Integer balance;

    public AccountUnsafe(Integer balance) {
        this.balance = balance;
    }

    @Override
    public Integer getBalance() {
        return this.balance;
    }

    @Override
    public void withdraw(Integer amount) {
        this.balance -= amount;
    }
}

//测试类
public class TestAccount {
    public static void main(String[] args) {
        Account account = new AccountUnsafe(10000);
        Account.demo(account);
    }
}

1、解决方法:使用synchronized解决线程安全问题

//对成员变量使用同步保证线程安全
class AccountUnsafe implements Account {

    private Integer balance;

    public AccountUnsafe(Integer balance) {
        this.balance = balance;
    }

    @Override
    public Integer getBalance() {
        synchronized (this) {
            return this.balance;
        }
    }

    @Override
    public void withdraw(Integer amount) {
        synchronized (this) {
            this.balance -= amount;
        }
    }
}

2、解决方法:使用无锁CAS解决线程安全问题

//使用无锁的方式也能保证线程安全
class AccountCas implements Account {

    private AtomicInteger balance;

    public AccountCas(int balance) {
        this.balance = new AtomicInteger(balance);
    }

    @Override
    public Integer getBalance() {
        return balance.get();
    }

    @Override
    public void withdraw(Integer amount) {
        while (true) {
            // 获取余额的最新值
            int prev = balance.get();
            // 修改余额
            int next = prev - amount;
            // 真正修改
            /*
                compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值
                - 不一致了,next 作废,返回 false 表示失败
                比如,别的线程已经做了减法,当前值已经被减成了 990
                那么本线程的这次 990 就作废了,进入 while 下次循环重试
                - 一致,以 next 设置为新值,返回 true 表示成功
            */
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。

在这里插入图片描述

CAS有三个操作数,旧值prev,主存中的新值,要更改成的新值next。当且仅当旧值prev和主存中的新值一致时,才会将主存中的值更改为新值next,否则什么也不做。

上面的流程图:线程1和线程2同时更新同一变量Account对象的值

在这里插入图片描述

(1) 线程1和线程2从主存中读取Account=100到自己的工作内存中

(2) 线程1将Account=100修改为90,并将结果同步到主存中(写屏障的原因),因此此时主存中Account=90

(3) 线程2向要通过CAS操作将Account变量的值修改为90,于是在set之前就会重新读取主存的值(读屏障的原因)并与工作内存中的值进行比较,如果相同就说明没有其他线程更改共享数据,成功的将主存中的值修改为90,但是如果不一致就说明CAS失败,此时就会进行下一次CAS操作(CAS自旋,前提在while循环内)

具体执行流程图:

在这里插入图片描述

2、CAS需要volatile的支持

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。

注意:

volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原
子性)

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

public class AtomicInteger extends Number implements java.io.Serializable {
    private volatile int value;
  
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
}

3、为什么CAS+volatile效率更高?

无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时
候,发生上下文切换,进入阻塞。

打个比喻:
线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,
等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑
道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还
是会导致上下文切换。

结合 CAS + volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再
重试呗。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想
改,我改完了解开锁,你们才有机会。

CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思。因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一,但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。

CAS缺点:

  1. CPU开销较大,多线程反复尝试更新某一个变量的时候容易出现;
  2. 不能保证代码块的原子性,只能保证变量的原子性操作;
  3. ABA问题

4、AtomicInteger?

注意:如果是这个问题,就是在问你CAS,你只需要将CAS讲一遍,然后再加上本类的特点即可。

class AccountCas implements Account {
    private AtomicInteger balance;
    
    public AccountCas(int balance) { this.balance = new AtomicInteger(balance);}
    
    @Override
    public Integer getBalance() {return balance.get();}

    @Override
    public void withdraw(Integer amount) {
        while (true) {
            int prev = balance.get();
            int next = prev - amount;
            // 真正修改(因为该变量balance是AtomicInteger类型的,因此可以调用该方法)
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

除了AtomicInteger类中的CAS方法外,还有其他封住的一些比较方便的方法(CAS需要放在while循环中):

AtomicInteger i = new AtomicInteger(0);

// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());

// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());
// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());

// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));

// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));

将上面的CAS操作改成下面的方法会简便很多:不需要使用while循环,并且简化了操作步骤

class AccountCas implements Account {
    private AtomicInteger balance;
    
    public AccountCas(int balance) { this.balance = new AtomicInteger(balance);}
    
    @Override
    public Integer getBalance() {return balance.get();}

    @Override
    public void withdraw(Integer amount) {
       balance.getAndAdd(-1*amount)
    }
}

5、CAS和ABA的问题如何解决?

AtomicReference(原子引用)
AtomicMarkableReference
AtomicStampedReference

class DecimalAccountCas implements DecimalAccount {
    private AtomicReference<BigDecimal> balance;

    public DecimalAccountCas(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

    //获取余额
    @Override
    public BigDecimal getBalance() {
        return balance.get();
    }

    @Override
    public void withdraw(BigDecimal amount) {
        while(true) {
            BigDecimal prev = balance.get();
            BigDecimal next = prev.subtract(amount);
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

1、什么是ABA问题?

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了

@Slf4j(topic = "c.TestCAS")
public class TestCAS {
    static AtomicReference<String> ref = new AtomicReference<>("A");
    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        //获取共享变量的旧值A
        String prev = ref.get();
		//调用other()方法
        other();

        Thread.sleep(1000);
        
        // CAS操作将A改为C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
    }

    //其他线程将共享变量从A改成B,又从B改成A,但主线程感知不到,主线程只会判断最新获取到的值A与prev是否相同
    private static void other() throws InterruptedException {
        new Thread(() -> {
            //线程t1将A改成B
            log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
        }, "t1").start();

        Thread.sleep(500);

        new Thread(() -> {
            //线程t2将B改成A
            log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
        }, "t2").start();
    }
}

执行结果:

14:05:10.388 c.TestCAS [main] - main start...
14:05:10.544 c.TestCAS [t1] - change A->B true
14:05:11.049 c.TestCAS [t2] - change B->A true
14:05:12.047 c.TestCAS [main] - change A->C true

在这里插入图片描述

如图所示:

(1) t1线程读取主存中的Ref引用变量的值A到自己的工作内存中,将其从A改为了B,并同步到了主存

(2) t2线程读取主存中的Ref引用变量的值B到自己的工作内存中,将其从B改成了A,并同步到了主存

(3) 此时主存中的值经历了A----》B---》A的过程,但是主线程感知不到

(4) 主线程进行CAS操作将Ref变量的值更改为了C(将工作线程中的值和主存中的值进行比较,发现一致,就认为主存中的共享变量的值没有更改过)

主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,如果主线程
希望:只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号 。

2、AtomicStampedReference

在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。 从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

@Slf4j(topic = "c.TestCAS1")
public class TestCAS1 {
    //原子引用变量的初始值为A
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        //获取旧值A
        String prev = ref.getReference();
        // 获取版本号
        int stamp = ref.getStamp();
        log.debug("版本 {}", stamp);

        // 如果中间有其它线程干扰,发生了 ABA 现象
        other();

        Thread.sleep(1000);

        // 主线程尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }

    private static void other() throws InterruptedException {
        //每次更新成功后,将版本号加1
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t1").start();

        Thread.sleep(500);

        //每次更新成功后,将版本号加1
        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t2").start();
    }
}

执行结果:

14:04:28.851 c.TestCAS1 [main] - main start...
14:04:28.856 c.TestCAS1 [main] - 版本 0
14:04:28.977 c.TestCAS1 [t1] - change A->B true
14:04:28.977 c.TestCAS1 [t1] - 更新版本为 1
14:04:29.482 c.TestCAS1 [t2] - change B->A true
14:04:29.483 c.TestCAS1 [t2] - 更新版本为 2
14:04:30.477 c.TestCAS1 [main] - change A->C false

在这里插入图片描述

(1) 线程1从主存中将Ref引用变量的值和Stamp版本号的值读入到工作内存中,并将Ref从A改成了B,Stamp从0改成了1

(2) 线程1从主存中将Ref引用变量的值和Stamp版本号的值读入到工作内存中,并将Ref从B改成了A,Stamp从1改成了2

(3) 此时主存中的Ref引用变量的值为A,Stamp版本号的值为2

(4) 主线程在进行CAS操作时,会将主存中的Ref变量的值A(新值)和工作内存中Ref变量的值A(旧值)进行比较,判断是否相同(相同),同时还会将主存中Stamp版本号的值2(新值)和工作内存中Stamp版本号的值0(旧值)进行比较,判断是否相同(不同),只有两者都相同,CAS才会成功。

AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A ->
C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。

但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过

3、AtomicMarkableReference

AtomicMarkableReference可以理解为上面AtomicStampedReference的简化版,就是不关心修改过几次,仅仅关心是否修改过。因此变量mark是boolean类型,仅记录值是否有过修改。

@Slf4j(topic = "c.Test38")
public class Test38 {
    public static void main(String[] args) throws InterruptedException {
        GarbageBag bag = new GarbageBag("装满了垃圾");
        // 参数2 mark 可以看作一个标记,表示垃圾袋满了
        AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);

        log.debug("start...");
        //获取垃圾袋
        GarbageBag prev = ref.getReference();
        log.debug(prev.toString());

        new Thread(() -> {
            log.debug("start...");
            bag.setDesc("空垃圾袋");
            //保洁阿姨将垃圾桶的垃圾倒空,并将垃圾状态从true(满)改为false(空)
            ref.compareAndSet(bag, bag, true, false);
            log.debug(bag.toString());
        },"保洁阿姨").start();

        sleep(1);
        log.debug("想换一只新垃圾袋?");
        //房东想要更换垃圾袋,发现垃等换失败,因为垃圾袋状态为false(空),但是期待的为true(满)
        boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
        log.debug("换了么?" + success);
        log.debug(ref.getReference().toString());
    }
}

class GarbageBag {
    String desc;

    public GarbageBag(String desc) {
        this.desc = desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return super.toString() + " " + desc;
    }
}

执行结果:

14:43:55.104 c.Test38 [main] - start...
14:43:55.113 c.Test38 [main] - cn.itcast.test.GarbageBag@769c9116 装满了垃圾
14:43:55.254 c.Test38 [保洁阿姨] - start...
14:43:55.254 c.Test38 [保洁阿姨] - cn.itcast.test.GarbageBag@769c9116 空垃圾袋
14:43:56.277 c.Test38 [main] - 想换一只新垃圾袋?
14:43:56.277 c.Test38 [main] - 换了么?false
14:43:56.277 c.Test38 [main] - cn.itcast.test.GarbageBag@769c9116 空垃圾袋

在这里插入图片描述

(1) 保洁阿姨线程从主存中读取bag变量到工作内存中,并将当前垃圾袋倒空,但是并没有更换垃圾袋同时将flag的状态从true更改为false然后同步到主存中。

(2) 此时主存中的bag还是原来的bag(还是原来的垃圾袋),标记变量boolean flag = false

(3) 此时房东线程想要通过CAS更换垃圾袋,首先将工作内存中的垃圾袋bag和主存中的垃圾袋bag进行比较,判断是否相同(相同),然后将工作内存中的flag=true与主存中的flag=false进行比较,判断是否相同(不同),自由两个都相同,CAS才能成功。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章