Detailed CAS mechanism

1. Definitions

CAS is an abbreviation of the English word Compare and Swap, which translates to compare and replace.

2. The principle

  • Uses three basic mechanisms operands CAS: memory address V, the expected value of the old A, to modify the new value of B.
    • Updating a variable, only when the actual value of the expected value of the variable A and V which memory addresses are the same, only the memory address corresponding to modify the value V B.

3. No real explanation CAS version number

  1. Among the memory address V, it is stored in the variable value of 10.
       cas01

  2. At this time, the value of the variable thread 1 wants to increase the thread 1 1, the expected value of the old A = 10, to modify the new value B = 11.
    cas02

  3. 1 to submit before the thread updates, another thread 2 the first step, the memory address of the variable value V became the first to update 11.
    cas03

  4. Thread 1 begin submitting updated first address the actual values ​​of A and V of comparison and found that A is not equal to the actual value of V, submission failed.
    cas04

  5. Thread 1 reacquire the current value of V memory address, and re-calculate the new value you want to modify. At this time, thread 1 is, A = 11, B = 12 . This process is called re-try the spin .
    cas05

  6. This time lucky, no other thread change the value of V addresses. Thread 1 for the Compare , found that the actual values of A and V addresses are equal.
    cas06

  7. Thread 1 is the SWAP , the value of the address is replaced V as B, is 12.
    cas07

  • 从思想上来说,Synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守
  • CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。

4. CAS机制在Java中的应用

  • 在Java中除了上面提到的Atomic系列类,还有Lock系列类的底层实现
  • 在Java 1.6以上版本,Synchronized转变为重量级锁之前,也会采用CAS机制。

5. CAS的缺点

1. CPU开销过大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

2. 不能保证代码块的原子性

  • CAS机制保证的只有一个变量的原子性操作,而不能保证整个代码块的原子性。
    • 比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

3. ABA问题

这是CAS机制最大的问题所在。

6. JAVA中CAS的底层实现

1. AtomicInteger当中常用的自增方法incrementAndGet

public final int incrementAndGet() {

    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }

}

private volatile int value; 

public final int get() {

    return value;

}


这段代码是一个无限循环,也就是CAS的自旋,循环体中做了三件事:
1. 获取当前值
2. 当前值+1,计算出目标值
3. 进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤
  • 这里需要注意的重点是get方法,这个方法的作用是获取变量的当前值。
  • 如何保证获取的当前值是内存中的最新值?
    • volatile关键字来保证(保证线程间的可见性)。

2. compareAndSet方法的实现,以及方法所依赖对象的来历

public final boolean compareAndSet(int expect, int update) {
    return unsafe.cmpareAndSwapInt(this, valueOffset, expect, update);
}

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); }
}

compareAndSet方法的实现很简单,只有一行代码。这里涉及到两个重要的对象,一个是unsafe,一个是valueOffset

  • 什么是unsafe呢?
    • Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作

至于valueOffset对象,是通过unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存地址。

我们上面说过,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B

而unsafe的compareAndSwapInt方法的参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。

正是unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。

7. CAS的ABA问题原理

  1. 假设内存中有一个值为A的变量,存储在地址V中。
         cas08

  2. 此时有三个线程想使用CAS的方式更新这个变量的值,每个线程的执行时间有略微偏差。线程1和线程2已经获取当前值,线程3还未获取当前值。
    cas09

  3. 线程1先一步执行成功,把当前值成功从A更新为B;同时线程2因为某种原因被阻塞住,没有做更新操作;线程3在线程1更新之后,获取了当前值B。
    cas10

  4. 线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新成了A。
    cas11

  5. 线程2终于恢复了运行状态,由于阻塞之前已经获得了“当前值A”,并且经过compare检测,内存地址V中的实际值也是A,所以成功把变量值A更新成了B。
    cas12

这个过程中,线程2获取到的变量值A是一个旧值,尽管和当前的实际值相同,但内存地址V中的变量已经经历了A->B->A的改变。

看起来这个例子没啥问题,但如果结合实际,就可以发现它的问题所在。

  1. 我们假设一个提款机的例子。假设有一个遵循CAS原理的提款机,小灰有100元存款,要用这个提款机来提款50元。
    cas13

  2. 由于提款机硬件出了点问题,小灰的提款操作被同时提交了两次,开启了两个线程,两个线程都是获取当前值100元,要更新成50元。
    理想情况下,应该一个线程更新成功,一个线程更新失败,小灰的存款值被扣一次。
    cas14

  3. 线程1首先执行成功,把余额从100改成50.线程2因为某种原因阻塞。这时,小灰的妈妈刚好给小灰汇款50元。
    cas15

  4. 线程2仍然是阻塞状态,线程3执行成功,把余额从50改成了100。
    cas16

  5. 线程2恢复运行,由于阻塞之前获得了“当前值”100,并且经过compare检测,此时存款实际值也是100,所以会成功把变量值100更新成50。
    cas17

原本线程2应当提交失败,小灰的正确余额应该保持100元,结果由于ABA问题提交成功了。

8. CAS中ABA问题解决方案

1. 添加版本号

真正要做到严谨的CAS机制,在compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。

  1. 假设地址V中存储着变量值A,当前版本号是01。线程1获取了当前值A和版本号01,想要更新为B,但是被阻塞了。
    cas18

  2. 这时候,内存地址V中变量发生了多次改变,版本号提升为03,但是变量值仍然是A。

  3. 随后线程1恢复运行,进行compare操作。经过比较,线程1所获得的值和地址的实际值都是A,但是版本号不相等,所以这一次更新失败。

在Java中,AtomicStampedReference类就实现了用版本号作比较的CAS机制。

2. java语言CAS底层如何实现?

利用unsafe提供的原子性操作方法。

3. 什么是ABA问题?怎么解决?

  • 当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。
    • 利用版本号比较可以有效解决ABA问题。

9. 参考

Guess you like

Origin www.cnblogs.com/yueyun00/p/10929211.html