【精选】我知道乐观锁,但是我的确不知道CAS啊,到底什么是CAS-CSDN博客

20 阅读8分钟

目录

写在前面

一、初识CAS(比较并交换)

二、CAS原理(自旋锁、unsafe类)

三、CAS是什么

四、CAS缺点

五、ABA问题


写在前面

    相信很多小伙伴对乐观锁、悲观锁都不陌生,但是说到java的cas,就蒙圈了。

    那么到底什么是CAS呢?

一、初识CAS(比较并交换)

/**
* CAS
*/
public class CASDemo {

    public static void main(String[] args) {

        AtomicInteger atomicInteger = new AtomicInteger(5);
        /* compareAndSet(int expect, int update)
            两个参数 期望值,修改值
            假如说主物理内存的值与expect的值相等,则修改成功,否则修改失败。
         */
        System.out.println(atomicInteger.compareAndSet(5, 2021) + "现在值是" + atomicInteger.get());
        /*
            因为主物理内存的值已经变成了2021,再由5修改为2022,会执行失败返回false
         */
        System.out.println(atomicInteger.compareAndSet(5, 2022) + "现在值是" + atomicInteger.get());

    }
}

二、CAS原理(自旋锁、unsafe类)

1.之前解决i++不安全的问题,用到了一个这个方法:

public final int getAndIncrement() {
    // this:当前对象。
    // valueOffset:内存偏移量(内存地址)
    // 1 加1
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

2.源码:

3.源码解析

①Unsafe

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

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

②变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

③变量value用volatile修饰,保证了多线程之间的内存可见性。

三、CAS是什么

1.    CAS的全称为Conpare-And-Swap,它是一条CPU并发原语。

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

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

2.源码解读:

//一直循环,1.先得到当前数据,2.再进行比较并交换,3.如果交换成功就退出,交换失败就重新比较交换,直至成功。4.返回的是原值相当于i++

3.var解释:

var1:AtomicInteger对象本身。

var2:该对象值的引用地址。

var4:需要变动的数量。

var5:是用var1 var2找出的主内存中真实的值。

用该对象当前的值与var5比较:

如果相同,更新var5+var4并且返回true,如果不同,继续取值然后比较,直到更新完成。    

为什么用CAS而不用synchronized?

synchronized加锁,每次只有一个线程能访问资源。并发性下降。

CAS使用自旋锁,不需要手动加锁。

4.CAS线程时冲突执行步骤:假设线程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没有被挂起并执行compareAbdSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工一切OK。

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

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

5.

6.CAS小总结

    CAS(CompareAndSwap)比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。

    CAS应用:CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

四、CAS缺点

1.循环时间长,开销大

    我们可以看到getAndAddInt方法执行时,有个do-while。

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

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

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

3.引出来ABA问题。

五、ABA问题

1.ABA问题是怎么产生的

    CAS会导致“ABA问题”。

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

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

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

    中间过程如果不介意别人动过,那无所谓。

    中间过程别人不能动,那就有问题了。

2.解决ABA问题(加版本号)

①原子引用

import java.util.concurrent.atomic.AtomicReference;
/**
* 原子引用
* 如果想包装某个类,就用原子引用
*/
public class AtomicReferenceDemo {

    public static void main(String[] args) {
        User u1 = new User("zhangsan", 14);
        User u2 = new User("lisi", 15);
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(u1);
        // true 设置为lisi
        System.out.println(atomicReference.compareAndSet(u1, u2) + "当前值" +atomicReference.get().getName());
        // false 还是lisi
        System.out.println(atomicReference.compareAndSet(u1, u2) + "当前值" +atomicReference.get().getName());
    }
}

class User{
    private String name;
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

②时间戳的原子引用    ABA问题的彻底解决

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

/**
* ABA问题的解决
*/
public class ABADemo {

    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    // 初始化值、版本号
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
    public static void main(String[] args) {

        // 1.以下是ABA问题的产生
        new Thread(() ->{
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "t1").start();

        new Thread(() -> {
            try {
                // t2线程暂停1秒,确保t1线程完成了ABA操作
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 只认值,不认是否经历过ABA
            System.out.println(atomicReference.compareAndSet(100, 2019)
                + ",t2当前值" + atomicReference.get());
        }, "t2").start();


        // 2.以下是ABA问题的解决
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();// 版本号
            System.out.println("t3版本号" + stamp);
            // 暂停1秒钟t3线程,确保t4拿到相同的版本号
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 期望值、修改值、目前版本号、修改后版本号
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
        }, "t3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();// 版本号
            System.out.println("t4版本号" + stamp);
            // 暂停3秒钟t4线程,确保t3完成ABA
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 因为版本号已经被t3修改,所以这里比较并替换失败
            boolean b = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
            System.out.println(b + ",t4" + atomicStampedReference.getStamp() + "," + atomicStampedReference.getReference());
        }, "t4").start();

    }
}

六、使用CAS实现一个自旋锁

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

/**
 * 题目:实现一个自旋锁,复习CAS思想
 * 自旋锁好处:循环比较获取没有类似wait的阻塞。
 *
 * 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现
 * 当前有线程持有锁,所以只能通过自旋等待,直到A释放锁后B随后抢到。
 */
public class SpinLockDemo
{
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock()
    {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t"+"----come in");
        while (!atomicReference.compareAndSet(null, thread)) {

        }
    }

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

    public static void main(String[] args)
    {
        SpinLockDemo spinLockDemo = new SpinLockDemo();

        new Thread(() -> {
            spinLockDemo.lock();
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
            spinLockDemo.unLock();
        },"A").start();

        //暂停500毫秒,线程A先于B启动
        try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            spinLockDemo.lock();

            spinLockDemo.unLock();
        },"B").start();


    }
}

#