CAS--无锁的执行者

CAS:Compare And Swap,是一种无锁算法。在不使用锁即没有线程被阻塞的情况下实现多线程之间的变量同步。

1.CAS算法的核心算法原理

比较和交换是用于实现多线程同步的原子指令
是指将内存位置的内容与给定值进行比较。只有在相同的情况下,将该内存位置的内容修改为新的给定值。

涉及到三个操作数:
需要读写的内存值V;
进行比较的值A;
需要写入的新值B;
当且仅当比较值A与内存值V相同时,会将内存值修改为新值B并会返回true,否则什么也不做,并返回false。

这个过程是作为单个原子操作完成的。
比较和交换整体是一个原子操作。交换是一个不断重试的操作。
CAS是一种系统原语,原语的执行是连续的,在执行过程中不允许被中断

多个线程线程同时使用CAS操作一个变量时,只有一个会胜出并成功更新,其余均会失败。
失败的线程不会挂起,允许进行再次尝试,也允许失败的线程执行放弃操作不再重试
CAS是无锁操作,无锁操作中没有所锁的存在,不会出现死锁,即免疫死锁

  1. Java中执行CAS的实践者--Unsafe类

存在于sun.misc包中。
该类内部的方法可以像C的指针一样直接操作内存。类中的所有方法都是native修饰的,都可以直接调用操作系统底层资源执行相应任务。

Unsafe类中存在直接操作内存的方法

无锁CAS操作基于Unsafe类中的以下方法实现:

//第一个参数o为给定对象,offset为对象在内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值
//expected表示期望值,x表示要设置的值
//下面3个方法都通过CAS原子指令执行操作
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                  
 
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
 
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

补充Native方法
Native方法就是一个Java调用非Java代码的接口,该方法的实现由非Java语言实现。
定义native方法时,不提供实现体,实现体是由非Java语言在外面实现的。

native标识符暗示这个方法的实现体是非Java的,所有不可以和abstract标识符连用。
native方法对应的实现不是在当前文件,是在用其他语言实现的文件中。

调用Native方法
Java语言不能对操作系统底层进行访问和操作,可以通过JNI接口调用其他语言来实现对底层的访问。
JNI:Java Native Interface。
JNI是Java本机编程接口,是Java的SDK的一部分,允许Java代码使用以其他语言编写的代码和代码库

  1. 并发包Atomic中使用CAS原理实现的并发类

JUC包下的atomic包中的原子类就是通过CAS来实现了乐观锁。
JUC的atomic包下,是对Java部分数据类型的原子封装,在原有数据类型的基础上,提供了原子性的操作方法,保证了线程安全

java中的CAS的实现
看一下原子类AtomicInteger的源码定义:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    //获取指针类Unsafe
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //AtomicInteger的value实例在内存中的偏移地址
    private static final long valueOffset;

    static {
        try {
            //通过Unsafe类中的方法得到value变量的内存偏移地址
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
}

unsafe:获取并操作内存的数据。
Unsafe类是CAS的核心类。java中的CAS主要使用的是Unsafe类中的方法来进行操作特定内存的数据。

valueOffset:该AtomicInteger类的value变量在内存中的偏移地址。
Unsafe类就是根据内存偏移地址获取数据的。

value:存储的要进行原子操作的int值,使用volatile关键字修饰保证其在线程间是可见的。

AtomicInteger类在并发下的自增操作

   //AtomicInteger类中的自增方法
   /**
     * Atomically increments by one the current value.
     * 原子的增加一个当前值
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    //Unsafe类中的方法
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //当前线程根据偏移地址拿到内存中的value值
            var5 = this.getIntVolatile(var1, var2);
        //在while循环中不断进行内存位置的内容与给定值的比较,相等就会进行更新,不等就会不断重试
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

当AtomicInteger类进行并发下的对自己的value变量的自增操作时,比如说A和B两个线程同时进行自增。
a. A与B会先通过getIntVolatile()方法得到内存中的存储的value值存入自己的寄存器内存中。
b. 这时当线程A被挂起时,B线程会通过compareAndSwapInt()方法进行比较内存中的value值和自己所存储的value,当两者相等时就会进行内存值value的更新。
c. A线程恢复时,执行compareAndSwapInt()方法进行比较时会发现value值已经被其他线程修改了。
因为value被volatile修饰,所有B线程对内存的修改A线程是可以看到的?有问题

  1. CAS操作存在的问题

a. ABA问题

  • 线程初次读取特定内存位置的内容为A,在准备赋值时检查到还是A,能说明值没有被其他线程修改过吗?

首先:在ABA后,当前值与内存值比较结果相同的,如果在进行交换更新内存值后。为什么这个过程是有问题的?

ABA问题.png

如上图1是一个单向链表实现的堆栈,栈顶为A,线程1已经直到A.next为B,希望用CAS操作将栈顶替换为B。
在线程1进行操作之前,线程2将A、B出栈,将D、C、A push进栈,这时如图2所示。
当线程1再继续CAS操作将栈顶替换为B时,检测到栈顶还是A,这时候CAS操作会成功,栈顶变为B。

ABA问题.png

此时如图3所示,B.next = null,堆栈中只有一个B元素,会将C、D丢掉。

这就是为什么在ABA后当前值与内存值比较结果相同的,再进行更新内存值的这个过程是有隐患的有问题的原因。

怎么解决ABA问题?
思路:给值加一个修改版本号,每次值变化都会修改值得版本号,每次得CAS操作时都对比版本号。

Java中使用AtomicStampedReference类来解决ABA问题。AtomicStampedReference类中的内部类Pair中主要包含一个对象引用以及一个可以自动更新的整数stamp去解决ABA问题。

AtomicStampReference的部分源码

public class AtomicStampedReference {

    private static class Pair {
        //对象的引用
        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);
        }
    }

    private volatile Pair pair;
    ......
    //第一个参数:更新之前的原始值
    //第二个参数:将要更新的新值
    //第3个参数:期待更新的版本标志
    //第4个参数:将要更新的版本标志
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        //获取当前pair
        Pair current = pair;
       //如果
       //原始值等于当前pair的引用,则说明值未变化
       //原始标记等于当前pair的标记版本,说明版本未变化
       //新值等于旧值并且新版本等于旧版本就什么也不干
       //否则CAS操作,生成一个新的pair对象,替换掉旧的,修改成功返回true,失败返回false
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
}

b. 循环时间开销大
CAS操作长时间不成功时会导致线程一直自旋,给CPU带来很大的开销。

c. 只能保证一个共享变量的原子操作
对一个共享变量操作时,CAS可以保证原子操作;对多个共享变量操作时,CAS无法保证操作的原子性。

DLL?
本地代理
JNI
volatile该看了
为啥对多个共享变量就不可以保证其原子性?

这几点来看:
核心算法原理
Java执行CAS的实践者Unsafe类
并发包Atomic中使用CAS原理实现的并发类
CAS存在的问题

你可能感兴趣的:(CAS--无锁的执行者)