深入理解 CAS


先来看下下面这段代码

package juc;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author Woo_home
 * @create by 2020/3/21
 */
public class CASDemo {
    public static void main(String[] args) {
        // 原始值为 5
        AtomicInteger atomicInteger = new AtomicInteger(5);
        // 因为原始值和期望值相等,则更新值为 2019
        System.out.println(atomicInteger.compareAndSet(5,2019) + " current data : " + atomicInteger.get());
        // 由于上述已经将值改为 2019,所以与期望值比较不相等,不能更新值
        System.out.println(atomicInteger.compareAndSet(5,1024) + " current data : " + atomicInteger.get());
        // 获取最后修改成功的值
        System.out.println(atomicInteger.getAndIncrement());
    }
}

输出结果
在这里插入图片描述
可以发现 atomicInteger.getAndIncrement() 是一个原子的过程,因为获取到的是最新的值,那么这个方法是如何做到的呢?
在这里插入图片描述
该方法底层调用的是 Unsafe 类的 getAndAddInt 方法

  • this:代表的是当前对象
  • valueOffset:内存偏移量
  • 1:以原子方式将当前值增加一(固定的)

getAndAddInt

以下为 Unsafe 类的 getAndAddInt 方法

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
    	// 获取当前对象和内存地址偏移量
        var5 = this.getIntVolatile(var1, var2);
        // 如果当前对象(var1)的值(var2)跟(var5)一样,那么(var5 + 1)
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
  • var1:AtomicInteger 对象本身
  • var2:该对象值的引用地址
  • var4:需要变动的数量
  • var5:是通过 var 1 、var 2 找出的主内存中的真实的值

用该对象当前的值与 var 5 进行比较
如果相同,则更新 var5+var4 并且返回 true
如果不同,则继续取值然后再比较,直到更新完成


在这里插入图片描述
假设线程 A 和线程 B 两个线程同时执行 getAndAddInt 操作(分别跑在不同的 CPU 上):

(1)AtomicInteger 里面的 value 原始值是 3,即主内存中 AtomicInteger 的 value 为 3,根据 JMM 模型,线程 A 和线程 B 各自持有一份值为 3 的 value 的副本分别到各自的工作内存

(2)线程 A 通过 getIntVolatile(var1,var2) 拿到 value 值 3,这时线程 A 被挂起

(3)线程 B 也通过 getIntVolatile(var1,var2) 方法获取到 value 值 3,此时刚好线程 B 没有被挂起 并执行 compareAndSwapInt 方法比较内存值也为 3,成功修改内存值为 4,线程 B 打完收工,一切 OK

(4)这时线程 A 恢复,执行 compareAndSwapInt 方法比较,发现自己手里的值数字 3 和主内存中的值数字 4 不一致,说明该值已经被其它线程抢先一步修改过了,那 A 线程本次修改失败,只能重新读取重新来一遍了

(5)线程 A 重新获取 value 值,因为变量 value 被 volatile 修饰,所以其它线程对它的修改,线程 A 总是能够看到,线程 A 继续执行 compareAndSwapInt 进行比较替换,直到成功

Unsafe

在 AtomicInteger 中有这么一段代码
在这里插入图片描述

// 获取 Unsafe 这个类
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 获取对象地址在这个内存中的偏移量
valueOffset = unsafe.objectFieldOffset
// value 被 volatile 修饰,保证原子性
private volatile int value;

Unsafe是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe 相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe 类存在于 sun.misc 包中,其内部方法操作可以像 C 的指针一样直接操作内存,因为 Java 中 CAS 操作的执行依赖于 Unsafe 类的方法

注意 Unsafe 类中的所有方法都是 native 修饰的,也就是说 Unsafe 类中的方法都是直接调用操作系统底层资源执行相应任务

CAS

CAS 的全称为 Compare-And-Swap,它是一条 CPU 并发原语

优点

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的

CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类中的各个方法,调用 Unsafe 类中的 CAS 方法,JVM 会帮我们实现出 CAS 汇编指令。这是一种完全依赖于 硬件 的功能,通过它实现了原子操作。再次强调,由于 CAS 是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS 是一条 CPU 的原子指令,不会造成所谓的数据不一致问题

缺点

自旋(循环时间长,开销大)

上面讲过 getAndAddInt 方法执行时,有个 do while 循环
在这里插入图片描述

如果 CAS 失败,会一直进行尝试。如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销

只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁来保证原子性

ABA 问题

ABA 问题是怎么产生的?

CAS 算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差会导致数据的变化。

比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且线程 two 进行了一些操作将值变成了 B,然后线程 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后线程 one 操作成功

尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的

```java
package juc;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @author Woo_home
 * @create by 2020/3/23
 * ABA 问题解决
 */
public class ABADemo {

    static AtomicReference<Integer> reference = new AtomicReference<>(100);

    public static void main(String[] args) {
        System.out.println("==================以下是 ABA 问题的产生======================");
        new Thread(() -> {
            reference.compareAndSet(100,101);
            reference.compareAndSet(101,100);
        },"t1").start();

        new Thread(() -> {
            try {
                // 暂停1秒钟t2线程,保证上面的t1线程完成了一次 ABA 操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(reference.compareAndSet(100,2019) + " " + reference.get());
        },"t2").start();
    }
}

从代码中可以发现,t1 线程将值 100,改为了 101,然后再从 101 改回了 100,然而 t2 以为 t1 并没有修改过值,所以 t2 将值修改为 2019
在这里插入图片描述

解决 ABA 问题

原子引用
package juc;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @author Woo_home
 * @create by 2020/3/23
 * ABA 问题解决
 */
public class ABADemo {

    static AtomicReference<Integer> reference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
    public static void main(String[] args) {
        System.out.println("==================以下是 ABA 问题的产生======================");
        new Thread(() -> {
            reference.compareAndSet(100,101);
            reference.compareAndSet(101,100);
        },"t1").start();

        new Thread(() -> {
            try {
                // 暂停1秒钟t2线程,保证上面的t1线程完成了一次 ABA 操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(reference.compareAndSet(100,2019) + " " + reference.get());
        },"t2").start();

        System.out.println("==================以下是 ABA 问题的解决======================");

        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + " 第1次版本号: " + stamp);
            try {
                // 暂停1秒钟t3线程
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + " 第2次版本号: " + stampedReference.getStamp());
            stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + " 第3次版本号: " + stampedReference.getStamp());
        },"t3").start();

        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + " 第1次版本号: " + stamp);
            try {
                // 暂停3秒钟t4线程
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result = stampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + " 修改成功否: " + result + " 当前最新实际版本号: " + stampedReference.getStamp());
            System.out.println(stampedReference.getStamp() + " 当前实际最新值: " + stampedReference.getReference());
        },"t4").start();
    }
}

输出:
在这里插入图片描述
输出结果中黄色部分是 t1,t2 线程的,其它的是 t3,t4 线程的

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值