CAS记录

CAS

CAS(Compare-And-Swap)是什么?

它是一条CPU并发原语, 它的功能是判断内存某个位置的值是否为预期值, 如果是则更改为新的值, 整个过程是原子的;
CAS并发原语体现在Java语言中就是sun.misc.Unsafe类中的各个方法. 调用Unsafe中的CAS方法, JVM会帮我们实现出CAS汇编指令, 这是一种完全依赖于硬件的功能, 通过它实现了原子操作. 再次强调, CAS是一种系统原语, 原语属于操作系统用于范畴, 是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成数据不一致问题.
比较并交换; 当真实值和期望值相同时则进行交换并返回true, 当真实值和期望值不同时不对内存进行操作并返回false;

public class CASDemo{

    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);

        // main do something

        // 修改成功, 输出 true, 100
        System.out.println(atomicInteger.compareAndSet(5, 100) + "\t current data: " + atomicInteger.get());
        // 修改失败, 输出 false, 100
        System.out.println(atomicInteger.compareAndSet(5, 200) + "\t current data: " + atomicInteger.get());
    }
}

Unsafe类

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

例如: AtomicInteger.getAndIncrement()方法中


/**
* this 调用该方法的对象;
* valueOffset 指的是内存中的偏移地址, 即对应数值在内存中对应的地址
* 1 表示步进值.
*/
public final int getAndIncrement(){
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

为什么CAS不用synchronized

加synchronized表示加锁, 强制一致性但是无法保证并发性; CAS内部是一个在不断比较的while循环, 在保证数据一致性的同时可以保证并发性;

JMM模型相关

在多线程执行AtomicInteger.getAndIncrement();模拟情况:

存在共享变量 private volatile AtomicInteger value = 3;
假设线程A和线程B两个线程被同时执行getAndAddInt操作(分别在不通的CPU上运行)

  1. 主内存中的共享变量值为3, 线程A和线程B会各自持有一份值为3的value的副本到各自的工作内存.
  2. 线程A通过getIntVolatile(var1, var2) 拿到value值为3, 此时A被挂起
  3. 线程B通过getIntVolatile(var1, var2) 拿到value值为3, 此时线程B刚好没有被挂起并执行了compareAndSwapInt方法, 比较内存值也为3, 然后成功修改内存值为4, 线程B运行完成.
  4. 此时线程A恢复, 开始执行compareAndSwapInt方法比较, 发现线程工作内存中的3和主内存中的共享变量值4不相等, 说明已经被其他线程修改过了, 那A本次的修改就会失败, 只能重新读取再来一次.
  5. 线程A重新获取共享变量的value值, 因为value被volatile修饰, 线程A总是能看到, 线程A继续执行compareAndSwapInt方法进行比较替换, 知道成功;

Unsafe类 + CAS思想(自旋)

自旋锁(spinlock)
是指尝试获取锁的线程不会立即阻塞, 而是采用循环的方式去尝试获取锁, 这样的好处是减少线程上下文切换的消耗, 缺点是循环会消耗CPU.

  • 短时代码可以使用自旋锁搞定, 如果锁住部分代码执行时间过长会导致CPU占用过高, 随时有被线程中断的风险
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class SpinLockDemo {
    private static final AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void tryLock() {
        Thread thread = Thread.currentThread();
        while (!atomicReference.compareAndSet(null, thread)) {
        }
        System.out.println("Thread-" + thread.getName() + "获得锁");
    }

    public void unLock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println("Thread-" + thread.getName() + "解锁");
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(() -> {
            spinLockDemo.tryLock();
            try {
                TimeUnit.SECONDS.sleep(5L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.unLock();
        }, "A").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.tryLock();
            spinLockDemo.unLock();
        }, "B").start();
    }
}

CAS缺点

  1. 循环时间长开销很大
  2. 只能保证一个共享变量的原子性
  3. ABA问题

ABA 问题

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

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

虽然线程one的CAS操作成功, 但是不代表这个过程是没有问题的;

只管结果不管过程.

在原子引用(原子引用 AtomicReference)中通过添加版本号和原始数据来避免ABA问题的出现

AtomicStampedReference(带时间戳的原子引用), 通过时间戳判断来解决ABA问题, stamp可以认为是版本号如果版本号和期望不一致则认为中间变量是被修改过的;

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

public class ABADemo {
    static AtomicReference<Integer> integerAtomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t stamp-1:" + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            try {
                TimeUnit.SECONDS.sleep(1L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t stamp-2:" + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t stamp-3:" + atomicStampedReference.getStamp());
        }, "t1").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t stamp-1:" + atomicStampedReference.getStamp());
            System.out.println(atomicStampedReference.compareAndSet(100, 2000,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1) + "\t current value: "
                    + atomicStampedReference.getReference() + "\t current stamp: " + atomicStampedReference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(3L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}

posted @ 2021-12-14 14:20  Hugiee  阅读(52)  评论(0)    收藏  举报