java并发之CAS(Compare and swap)

1. 简介

CAS的底层调用native方法,最终是利用CPU的一个特殊指令,该指令由CPU保证了原子性,而且包含多个操作,比如先比较再更新。

原理:

  • (1)需要读写的内存值(V)、原值(A)和新值(B)。如果V的值与原值A相匹配,那么把B设置给V,否则处理器不做任何操作。
  • (2)无论哪种情况,都返回V内存值。
  • (3)原子类里,当失败时,就一直循环,直到成功。

(1)、(2)是在CPU和内存的层面来说的,(3)是在Java层面说的

比如 AtomicInteger中的源码中,就使用到了 CAS:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

2. CAS的等价代码

自己实现CAS:

/**
  *      模拟CAS操作,等价代码
 */
public class SimulatedCAS {
    private int value;

    /**
     * 模拟CAS操作
     * @param expectedValue 预期值
     * @param newValue 修改值
     * @return 原值
     */
    public synchronized int compareAndSwap(int expectedValue, int newValue) {
        int oldValue = value;

        if (oldValue == expectedValue) {
            value = newValue;
        }
        return oldValue;
    }
}

在多线程中调用上面自己实现的CAS:

/**
  *      两个线程竞争,其中一个修改落败
 */
public class TwoThreadCompetition implements Runnable{
    private int value;

    /**
     * 模拟CAS操作
     * @param expectedValue 预期值
     * @param newValue 修改值
     * @return 旧值
     */
    public synchronized int compareAndSwap(int expectedValue, int newValue) {
        int oldValue = value;

        if (oldValue == expectedValue) {
            value = newValue;
            System.out.println(Thread.currentThread().getName()+"将值从"+oldValue+"修改成"+newValue);

        }
        return oldValue;
    }

    //主函数
    public static void main(String[] args) throws InterruptedException {
        TwoThreadCompetition r = new TwoThreadCompetition();
        r.value=0;//默认值
        Thread thread1 = new Thread(r,"线程1");
        thread1.start();
        thread1.join();
        System.out.println(r.value);
        Thread thread2 = new Thread(r,"线程2");
        thread2.start();
        thread2.join();
        System.out.println(r.value);
    }

    @Override
    public void run() {
        compareAndSwap(0,1);
    }
}

image-20230611211632613

打印结果显示,只有第一个线程对 value 进行了修改,第二个线程没有修改,符合CAS的逻辑。

3. 应用场景

(1)乐观锁

乐观锁的实现就是利用CAS原理,比如数据库在修改时,是利用version字段来判断是否应该执行修改,而不是加锁。

(2)并发容器

如ConcurrentHashMap类中的put()方法

(4)原子类

这里以 AtomicInteger 的 getAndIncrement()方法 为例剖析如何利用CAS实现原子性的

  • AtomicInteger加载Unsafe工具,用来直接操作内存数据
  • 用Unsafe工具来实现底层操作
  • 用volatile修饰value字段,保证可见性
上面使用到了 Unsafe 类,那么他是个什么呢?
  • Unsafe是CAS的核心类。Java一般无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。
  • valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的,这样我们就能通过unsafe来实现CAS了
总结:

总之,getAndIncrement() 的实现原理是:先获取 value 在内存中的地址,然后使用该地址拿到原值,再调用 compareAndSwapInt 方法,将内存地址value,原值,和新值传进去,执行 CAS 方法,compareAndSwapInt 是 native 方法,由c++实现,如下

上面的 C++ 代码可以看出:方法中先想办法拿到变量value在内存中的地址;通过Atomic::cmpxchg实现原子性的比较和替换,其中参数x是即将更新的值,参数e是原内存的值。至此,最终完成了CAS的全过程。

4 CAS的缺点

(1)ABA 问题

什么是ABA?

假若一个变量初次读取是A,在compare阶段依然是A,但其实可能从读取A到后面的compare这段过程里,它先被改为B,再被改回A,而CAS是无法意识到这个问题的。CAS只关注了比较那一刻的值是否A,而无法清楚在此过程中变量的变更明细,这就是所谓的ABA漏洞。

如何解决ABA问题

可以采用数据库的乐观锁方式,采用version版本号,每次修改就给version版本号+1

(2)自旋时间过长

do-while的自旋循环,如果多线程竞争激烈的情况下,锁一直拿不到,他就会一直自旋,消耗CPU性能。

点我扫码关注微信公众号

文章来源:Java并发之CAS(Compare and swap)


个人微信:CaiBaoDeCai

微信公众号名称:Java知者

微信公众号 ID: JavaZhiZhe

谢谢关注!

posted @ 2023-06-13 22:07  Java知者  阅读(73)  评论(0编辑  收藏  举报