Java基础-并发编程-CAS

Java工程师知识树 / Java基础


无锁策略

对于并发控制而言,锁是一种悲观的策略。如果有多个线程同时需要访问临界区资源,就宁可牺牲性能让线程进行等待,,所以说锁会阻塞线程的执行。而无锁是一种乐观的策略,它会假设对资源的访问是没有冲突的,那自然也不需要等待,所以多个线程都是可以在不停顿的状态下持续执行的。

无锁的策略使用的是一种叫做比较交换的技术(CAS Compare and swap)来鉴别线程冲突的,一旦检测到冲突发生,就重试当前操作直到没有冲突为止。

CAS介绍

CAS(Compare and swap)即比较后(比较内存中的旧值与预期值)交换(将旧值替换成新值)。

CAS原理:在把数据更新到主内存时,再次读取主内存变量的值,如果现在变量的值与期望的值(操作初始时读取的值)一样就更新,否则就不更新。

CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)

CAS操作过程:

如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。

上述过程图示为:

image

模拟CAS操作

package com.study.thead.cas;

public class CASTest {

    public static void main(String[] args) {
        CASCounter casCounter = new CASCounter();
        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {
//                System.out.println(casCounter.getAndUpdate()+ "----" + casCounter.getV());//在CAS指令之前返回该位置的值
                System.out.println(casCounter.incrementAndGet()+ "----" + casCounter.getV());//incrementAndGet返回的是内存中的值(V)
            }).start();
        }
    }
}

//实现一个CAS
class CASCounter {

    private volatile long V;// 线程可见的数字 表示内存位置V
    public long getV() { //
        return V;
    }
    /**
     * 定义一个 compare and swap  用于比较A与V  并赋值B
     * @param A 预期原值(A)
     * @param B 新值(B)
     * @return
     */
    private boolean compareAndSwap(long A, long B) {
        synchronized (this) { //保证比较时的同步
            if (V == A) {//比较A与V
                V = B;
                return true;
            } else {
                return false;
            }
        }
    }


    // 定义一个获取内存中的值并可以将新值加一的方法
    public long getAndUpdate() {// jdk1.8自带方法  
        long prev; //
        long next ;
        // do...while语句:首先会执行一次do{}之内的语句,然后在while()内检查条件是否为真,如果条件为真的话,就会重复do...while这个循环,直至while()为假。
        do {
            prev = this.getV();//1. 获取内存中的值 prev代表预期原值(A)
            next = prev + 1; //2. 初始化next值 next代表新值(B)
        } while (!compareAndSwap(prev, next)); // 3. 比较内存中的值与预期原值(A) 不一样的话就将next+1
        //在CAS指令之前返回该位置的值
        return prev;//while内条件为假,即prev = this.getV()   单线程只执行一次,多线程可能会多次
    }

    // 定义一个获取内存中的值并可以将新值加一的方法
    public final long incrementAndGet() {// jdk1.7前通俗易通版写法
        long current,next;
        while (true){// 一直执行到内存中的值(V)与预期原值(A)相等为止
            current = this.getV();//获取内存中的值
            next = current + 1;//新值(B)
            if (compareAndSwap(current, next))//如果内存中的值(V)与预期原值(A)相等
                return next;// 内存中的值(V)与预期原值(A)相等 跳出循环 返回新值(B)
        }
    }
}

// 执行结果  9999----10000
// 执行结果  10000----10000

CAS的ABA问题

CAS实现原子操作有一个假设:共享变量的当前值与当前线程提供的期望相同,就认为这个变量没有被其他线程修改过。

实际上这个假设不一定总是成立的。比如下图,线程2将V修改为X后再修改为V,表现的情况与假设情况一致。

image

CAS的ABA问题:共享变量经历了A->B->A的更新。

解决ABA问题的方案:为共享变量引入一个版本号。

JDK中就有个这样的实例:AtomicStampedReference

public class AtomicStampedReference {

    private static class Pair { // AtomicStampedReference内部类用于对共享对象添加一个对应的版本号
        final T reference;
        final int stamp; // 对象对应的版本号
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static  Pair of(T reference, int stamp) {
            return new Pair(reference, stamp);
        }
    }
    ...
    //AtomicStampedReference 使用时需要传入一个版本号  
    //当带有时间戳的原子操作类AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新版本号initialStamp。
    public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }
    ...
    //比较时将对象版本号一起传入
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
    ...
}

总结:AtomicStampedReference 通过一个键值对Pair存储数据和版本号,在更新时对数据和版本号进行比较,只有两者都符合预期才会调用Unsafe的compareAndSwapObject方法执行数值和版本号替换,也就避免了ABA的问题。

自旋问题

atomic类会多次尝试CAS操作直至成功或失败,这个过程叫做自旋。

通过自旋的过程我们可以看出自旋操作不会将线程挂起,从而避免了内核线程切换,但是自旋的过程也可以看做CPU死循环,会一直占用CPU资源。

这种情形在单CPU的机器上是不能容忍的,因此自旋一般都会有个次数限制,即超过这个次数后线程就会放弃时间片,等待下次机会。

因此自旋操作在资源竞争不激烈的情况下确实能提高效率,但是在资源竞争特别激烈的场景中,CAS操作会的失败率就会大大提高,这时使用中重量级锁的效率可能会更高。当前,也可以使用LongAdder类来替换,它则采用了分段锁的思想来解决并发竞争的问题。

所以,自旋问题的解决方案有两个:1.自旋设定一个次数限制;2.使用分段锁

CAS的底层实现原理

JDK1.8针对CAS操作的相关源码(以java.util.concurrent.atomic.AtomicInteger为例)

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
 
    // 这个就是封装CAS操作的指针
    private static final Unsafe unsafe = Unsafe.getUnsafe();
  
    //原来内部的共享变量,就是这个value,并且使用volatile让其在多个线程之间可见
    private volatile int value;
 
    //初始化的构造函数
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
 
    //获取当前值
    public final int get() {
        return value;
    }
 
    //设置当前的共享变量的值
    public final void set(int newValue) {
        value = newValue;
    }
 
    //使用CAS操作设置新的值,并且返回旧的值
    public final int getAndSet(int newValue) {
        //使用指针unsafe类的三大原子操作方法之一
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
 
 
    //把expect与内部的value进行比较,如果相等,那么把value的值设置为update的值
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
 
    //返回value,并把value + 1 
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
 
    //自增,并且返回自增后的值
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
 
}

....

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}
public native int     getIntVolatile(Object o, long offset);
public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

Java提供了一个Unsafe类,其内部方法操作可以像C的指针一样直接操作内存,方法都是native的。

CAS操作主要是通过Unsafe类调用JNI的代码实现的。(JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。)

public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

对应的其他语言的相关代码有:

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:
 
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
/ alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

总结:CAS操作主要是通过Java提供的Unsafe类调用Native方法实现的。在把数据更新到主内存时,再次读取主内存变量的值,如果现在变量的值与期望的值(操作初始时读取的值)一样就更新,否则就不更新。变量的值与期望的值的比较操作是通过Native方法实现.

你可能感兴趣的:(Java基础-并发编程-CAS)