JUC并发编程——CAS

一、什么是CAS

        由于JVM的synchronized重量级锁涉及操作系统内核态下互斥锁的使用,因此其线程阻塞和唤醒都涉及进程在用户态和内核态频繁的切换,导致重量级锁开销大,性能低。CAS,Compare And Swap比较并替换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(E)新值(N)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。CAS也称为自旋锁,在一个(死)循环[for(;;)]里不断进行CAS操作,直到成功为止(自旋操作),实际上,CAS也是一种乐观锁。

Unsafe 提供的CAS方法

public final native boolean compareAndSetReference(Object o, long offset,
                                                       Object expected,
                                                       Object x);
public final native boolean compareAndSetLong(Object o, long offset,
                                                  long expected,
                                                  long x);
public final native boolean compareAndSetLong(Object o, long offset,
                                                  long expected,
                                                  long x);

 参数说明

o- 需要操作字段所在的对象

offset-需要操作字段的偏移量 相对对象头,在64位jvm虚拟机偏移量是12,因为markWord占64位,Class pointer(类对象指针)占32位,所以偏移量是12.

expected-期望值,也就是旧值

x-更新的值,也就是新值

在执行Unsafe的CAS方法时,这些方法值首先将内存位置的值与旧值比较,如果匹配那么CPU会自动将内存位置的值更新为新值,并返回true,如果不匹配,CPU不做任何操作,并返回false。当并发修改的线程少,冲突出现的机会少时,自旋次数也会减少,CAS的性能就会很高,反之冲突越多,自旋的次数越多,CAS的性能就会越低。 

二、JUC原子类

以AtomicInteger为例

public final int get(); //获取当前的值
public final int getAndSet(int newValue); //获取当前的值,然后设置新的值
public final int getAndIncrement() ;//获取当前的值,然后自增
public final int getAndDecrement() ; //获取当前的值,然后自减
public final int getAndAdd(int delta) ; //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update);//通过CAS方式设置整数值
public class AtomicIntegerDemo extends Thread {
    private AtomicInteger atomicInteger;

    AtomicIntegerDemo(AtomicInteger atomicInteger) {
        this.atomicInteger = atomicInteger;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100000; i++) {
            atomicInteger.getAndIncrement();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        AtomicIntegerDemo demo1 = new AtomicIntegerDemo(atomicInteger);
        AtomicIntegerDemo demo2 = new AtomicIntegerDemo(atomicInteger);
        AtomicIntegerDemo demo3 = new AtomicIntegerDemo(atomicInteger);
        demo1.start();
        demo2.start();
        demo3.start();
        demo1.join();
        demo2.join();
        demo3.join();
        System.out.println(atomicInteger.get());//300000

    }
}

 由上面执行结果可知AtomicInteger是一个原子的操作,并发操作是线程安全的。AtomicInteger主要通过CAS自旋+volatile的方案实现既保障了变量操作的线程安全性,又避免了synchronized重量级锁的开销。

三、ABA问题

        使用CAS操作内存数据时,数据发生过变化也能更新成功,如A——>B——>A,最后一个CAS预期数据A实际已经发生过更改,但也能修改成功,这就产生了ABA的问题。ABA的解决思路一般是使用版本号,每次变更都带上版本号。JDK提供了两个类AtomicStampedReference和AtomicMarkableReference解决ABA的问题。

        AtomicStampedReference在CAS的基础上增加了一个Stamp(印戳或者标记),使用这个标识可以判断数据是否发生变化。

 public static void main(String[] args) {
        String str1 = "aaa";
        String str2 = "bbb";
        //初始化AtomicStampedReference 初始值是aaa,印戳是1
        AtomicStampedReference reference = new AtomicStampedReference(str1, 1);
        //cas比较str1=aaa ,reference.getStamp()=1,就把str2的值更新到新值,印戳加1
        reference.compareAndSet(str1, str2, reference.getStamp(), reference.getStamp() + 1);
        //当前的值为:bbb印戳是:2
        System.out.println("当前的值为:" + reference.getReference() + "印戳是:" + reference.getStamp());

        boolean flag = reference.weakCompareAndSet(str2, "ccc", 2, reference.getStamp() + 1);
        //更新后的值为:bbb是否更新成功:false,更新失败,因为印戳4和2比较不对
        System.out.println("更新后的值为:" + reference.getReference() + "是否更新成功:" + flag);
    }

        AtomicMarkableReference是AtomicStampedReference的简化版,不关心修改过几次,只关心是否修改过,标志属性是boolean类型,其值只记录是够修改过。

public static void main(String[] args) {
        AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(1, true);
        boolean b = atomicMarkableReference.compareAndSet(1, 2, true, false);
        //是否更新成功true更新后的值:2更新后的标志 mark:false
        System.out.println("是否更新成功" + b + "更新后的值:" + atomicMarkableReference.getReference() + "更新后的标志 mark:" + atomicMarkableReference.isMarked());
        boolean b1 = atomicMarkableReference.compareAndSet(2, 3, true, false);
        //是否更新成功false更新后的值:2更新后的标志 mark:false
        System.out.println("是否更新成功" + b1 + "更新后的值:" + atomicMarkableReference.getReference() + "更新后的标志 mark:" + atomicMarkableReference.isMarked());
    }

总结:

1、操作系统层面的CAS是一条CPU原子指令(cmpxchg指令),由于该指令具有原子性,因此使用CAS不会造成数据不一致的问题。

2、使用CAS无锁编程步骤如下,获取字段中的期望值(旧值)与内存地址上的值作比较(没有修改之前的旧值),如果两个值相等,就把新值放在字段的内存地址上,失败则自旋。

3、并发线程越少,自旋越少CAS性能越高,反之并发线程越多,自旋的次数越多,CAS的性能就越低。

4、jdk中的原子类大都是使用CAS实现的。

5、解决ABA问题使用版本号,jdk中有两个类AtomicStampedReference和AtomicMarkableReference解决ABA的问题。

参考:

《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著

你可能感兴趣的:(JUC,JUC,并发编程,CAS)