Java中的CAS

  • Post author:
  • Post category:java


1.什么是CAS?

CAS:全称Compare and swap,字面意思:“比较并交换”,一个CAS涉及到一下的操作:

我们假设内存中原数据V,旧的预期值A,需要修改的新值B。

1. 比较A与V是否相等(比较)

2. 如果比较相等,将B写入V。(交换)

3. 返回操作是否成功。



CAS伪代码

】下面写的代码不是原子的,真正的CAS是一个原子的硬件指令完成的。这个伪代码只适合辅助理解CAS的流程;

boolean CAS(address, expectValue, swapValue) {
        if(&address == expectedValue) {
            &address = swapValue;
            return true;
        }
        return false;
    }

多个线程同时对一个公共资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。

CAS因为硬件给予支持,软件层面才能做到原子性。

2.CAS有哪些应用

2.1实现原子性

标准库中提供了java.util.concurrent.atomic包,里面的类都是基于这种方式来实现的。

典型的就是AtomicInteger类。其中的getAndIncrement相当于i++操作。

        AtomicInteger a = new AtomicInteger(0);
        //相当于i++
        a.getAndIncrement();



伪代码实现

class AtomicInteger {
    private int value;
    public int getAndValue() {
        int oldValue = value;
        while(CAS(value, oldValue, oldValue + 1) != true) {
            oldValue = value;
        }
        return oldValue;
    }
}

假设两个线程同时调用getAndIncrement。按照我们原来的思想如果不加锁,会产生线程安全的问题。

1)两个线程都读取 value 的值到 oldValue 中(oldvalue是栈内存上的值,每个线程都有自己的栈)

2)线程1先执行CAS操作。由于oldValue和value的值相同,直接进行对value赋值。

  • CAS是直接读写内存的,而不是操作寄存器的;
  • CAS的读内存,比较,写内存操作时一条硬件指令,是原子的。

3)线程2执行CAS操作,第一次CAS的时候发现oldValue和value不相等,不能进行赋值。因此需要进入循环。

在循环里重新读取value的值赋值给oldValue

4)线程2接下来第二次执行CAS,此时oldValue和value相同,于是直接执行赋值操作。

5)线程1和线程2返回各自的oldValue的值即可。

2.2实现自旋锁

基于CAS实现更加灵活的锁,获取到更多的控制权。



伪代码

class SpinLock {
    private Thread owner = null;
    public void lock() {
        //通过CAS看当前锁是否被某个线程持有
        //如果这个锁已经被其他的线程持有,那么就自选等待。
        //如果这个锁没有被别的线程持有,那么就把owner设为当前尝试加锁的线程
        while(!CAS(this.owner, null, Thread.currentThread())) {}
    }
    public void unlock() {
        this.owner = null;
    }
}

2.3CAS的ABA问题

2.3.1什么是ABA问题

ABA问题:

假设存在两个线程t1和t2。有一个共享变量num,初始值为A。

接下来,线程t1向使用CAS把num值改为Z,那么就需要

  • 先读取num的值,记录到oldNum变量中;
  • 使用CAS判定当前num的值是否为A,如果为A,就更改为Z

但是,在t1执行这两个操作之间,t2线程可能把num的值从A改成了B,又从B改成了A

线程t1的CAS是期望num不变就修改。但是num的值已经被t2给改啦,只不过又改成了A。这个时候t1究竟是否更新num的值为Z呢?

到这一步,t1线程无法区分但概念这个变量始终是A,还是经历了一个变化过程。

2.3.2 ABA问题引来的BUG

大部分的情况下,t2线程这样的一个反复横跳的改动,对于t1是否修改num是没有影响的。但是不排除一些特殊情况。

假设滑稽老哥有100快钱存款,滑稽想从ATM取50块钱。取款机创建了两个线程,并发的执行-50操作。

我们期望一个线程执行-50成功,另一个线程-50失败。

如果使用CAS的方式来完成这个扣款过程就可能出现问题。


正常过程

1)存款100,线程1获取到当前存款值为100,期望更新为50;线程2获取到当前存款为100,期望更新50

2)线程1执行扣款成功,存款被改为50.线程2阻塞等待中。

3)轮到线程2执行了,发现当前存款为50,和之前读到的100不同,执行失败。


异常的过程

1)存款100。线程1获取到当前存款为100,更新期望值50;线程2获取到当前线程为100,期望更新为50;

2)线程1执行扣款成功后,线程2阻塞等待中;

3)在线程2执行前,滑稽的朋友正好给滑稽转账,余额变为100!!

轮到线程2执行了,发现当前存款为100,和之前读到的100相同,再次执行了扣款操作。


这个时候,我们发现同一个扣款操作,却执行了两次!!!

同样的栗子:

有单向链表a->b->c,线程1:我们想让 c 改为 d。线程2:我们将ba节点删除。

当线程1从内存中读取到c的时候oldValue = c。线程2抢占式执行,删除了a,b,c节点又添加了c节点,线程2执行完毕。线程1执行,发现还是原来的c,我们将c节点改为d。这就出现了问题。

问题导致的原因有两点:线程抢占式执行,所有操作整体不具有原子性(获取主内存中的值和比较交换之间不具有原子性)

2.3.3解决方案

我们给要修改的值,引入版本号,在CAS比较数据当前值和旧的值的时候,也要比较版本号是否符合预期。

  • CAS操作在读取旧值的同时也要读取版本号;
  • 真正修改的时候,

    • 如果当前版本号和读到的版本号相同,则修改数据,版本号+1;
    • 如果当前版本号高于读到的版本号,就操作失败

3.相关面试题

1)讲解一下你理解的CAS机制

全程Compare and swap,即“比较并交换”。相当于通过一个原子的操作,同时完成“读内存,比较是否相等,修改内存”这三个步骤,本质上需要CPU指令的支持。

2)ABA问题怎么解决?

引入一个版本号,在CAS比较数据当前值和旧值的时候,也要比较版本号是否符合预期。如果版本号一致才能修改成功,如果不一致,则执行失败。



版权声明:本文为fengxia110原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。