一文了解CAS以及源码分析

声明:尊重他人劳动成果,转载请附带原文链接!学习交流,仅供参考!

一、什么CAS?

1、CAS简介

CAS 是compareAndSwap的简称,用中文表达则为比较并更新,简单的说,预期原值A和从某一内存中取得的值V两者相比较,如果预期原值A和内存值V相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。

2、CAS的三个操作数

  • 内存值(V)
  • 预期原值(A)
  • 新值(B)

2、用处

解决多线程并发安全问题,以前我们对一些多线程操作的代码都是使用synchronizeLock关键字,来保证线程安全的问题;但是锁机制会出现很多开销,例如加锁和释放锁会导致比较多的上下文切换和调度延时。并且像synchronize这种拥有不可中断的性质。则开销更大。

但是CAS的解决方法则为:去内存中获取值(java是无法直接操作底层操作系统,是通过本地native方法来进行访问,这里的native方法是用C/C++写的,但是JVM还是提供了一个类,这个类(Unsafe)提供了硬件级别的原子操作。),获取到了内存值,然后和我们预期原值进行比较,如果相等则更新内存中的值。如果不相等则用do...while采取重试等措施。直到修改完成。

二、应用场景及源码分析

  • 乐观锁

也叫非互斥同步锁、不会锁住被操作对象,底层是CAS实现

  • 原子类

实现原子操作,底层也是CAS。用AtomicInteger举例

源码分析

1、获取Unsafe类

在这里插入图片描述

2、通过Unsafe类中的objectFieldOffset()获取value在内存的偏移量

在这里插入图片描述

3、 用volatile修饰value字段,保证可见性

在这里插入图片描述

4、查看AtomicInteger中的compareAndSet(),可以看到有两个参数expectedValue(预期原值),newValue(更新值)

在这里插入图片描述

然后再进入compareAndSetInt(),就可以看到是一个native方法。其中offset就是偏移量。

在这里插入图片描述

5、native方法在(C++)

在这里插入图片描述

6、通过偏移量获取地址

在这里插入图片描述

7、通过Atomic::cmpxchg()实现原子性的比较和替换,其中参数x是即将更新的值,参数e是原内存中的值,参数addr是获取内存中值的地址

在这里插入图片描述
总结

AtomicInteger中是通过Unsafe类来操作底层,尤其是Unsafe类中的compareAndSwapInt方法,方法中先想办法拿到变量value在内存中的偏移量。然后C++代码中通过偏移量拿到在内存的具体地址,然后通过Atomic::cmpxchg实现原子性的比较和替换,其中参数x是即将更新的值,参数e是原内存中的值。至此,最终完成了CAS的全过程。

  • 并发容器(ConcurrentHashMap)
    在这里插入图片描述

三、等价代码实现

1、等价代码实现

CAS是通过cpu的特殊指令来保证了原子性。(必须需要cpu支持这个指令)

下面所有代码就可以等于cpu中的一条指令

/**
 * @author delingw
 * @version 1.0
 * CAS等价代码
 * 先比较再更新
 */
public class CompareAndSwapDemo {
    
    
    private volatile int value;
    public synchronized int compareAndSwap(int exceptedValue, int newValue) {
    
    
        int oldValue = value;
        if (exceptedValue == oldValue) {
    
    
            value = newValue;
        }
        return oldValue;
    }
}

四、CAS中的缺点(ABA问题),怎么解决?

1、什么是ABA问题?

在执行conpareAndSwap的时候,它只是检查预期原值和内存中的值是不是相等,但是它并不检查在此期间是否被修改过,例如:线程一拿到这个值是5,然后就去计算了,在线程一计算的过程中,由第二个线程把5改为了7,然后又由第三个线程把7改会了5,等线程一计算完成后,去看当前值还是5,符合预期,那么它也会更新成功,从操作上看并没有什么不对,更新成功也是对的,但是这样是有隐患的。

解决方案

类似数据库乐观锁那种,添加版本号,

2、自旋时间长

解释一下自旋:就是如果一个线程准备去更新值,但是每次获取的值都被其他线程修改了,那么它就会一直去进行比较,直到成功为止,如果一直更新不了的话,那么CPU开销就会很大,这种要避免,所以一般对于这种适合并发写入少。大多数是读取的场景

代码展示

利用了AtomicReference类实现了一个乐观锁,乐观锁底层是用CAS实现的。所以会出现用do...while自旋。此处我用的while

/**
 * @author delingw
 * @version 1.0
 * 写一个乐观锁
 */
public class SpinLock {
    
    
    private AtomicReference<Thread> sign = new AtomicReference<>();

    // 加锁
    public void lock() {
    
    
        Thread current = Thread.currentThread();
        while (!sign.compareAndSet(null, current)) {
    
    
            System.out.println(Thread.currentThread().getName() + "获取锁失败,请重试");
        }
    }
    // 解锁
    public void unlock() {
    
    
        Thread current = Thread.currentThread();
        sign.compareAndSet(current, null);
    }
    public static void main(String[] args) {
    
    
        SpinLock spinLock = new SpinLock();
        Runnable r = new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println(Thread.currentThread().getName() + "开始尝试获取乐观锁");
                spinLock.lock();
                System.out.println(Thread.currentThread().getName() + "获取到了乐观锁");
                try {
    
    
                    Thread.sleep(300);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } finally {
    
    
                    spinLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放到了乐观锁");
                }
            }
        };
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
    }
}

结果

中间省略了一部分太多了

在这里插入图片描述
在这里插入图片描述

Guess you like

Origin blog.csdn.net/qq_40805639/article/details/121430149