java多线程之CAS操作AtomicInteger,AtomicIntegerArray,AtomicReference等原子操作类详解

余生长醉 提交于 2020-01-30 10:06:46

原子操作是什么

原子操作即不可被中断(分割)的一个或一系列操作,如对于A,B两个操作,如果一个线程操作A,若另一个线程执行B操作时,要么将B执行到结束,要么完全不让B执行,此时对于A和B来说,就是原子的。

悲观锁和乐观锁

synchronized就是悲观锁,解决多线程并发问题,以保证事务的完整性。其是基于阻塞的锁机制,如果有一个线程拥有锁后,访问该资源的其他线程就需要阻塞等待,直到获得锁的线程释放锁。
CAS操作即是乐观锁,每个线程都可以访问,只有在提交数据的时候检查是否违反了数据的完整性,即每次都尝试去完成某个操作,如果冲突和造成操作失败,就循环重试,直到操作成功为止。

使用synchronized会引出很多问题,如:获得锁的线程一直不释放锁;大量线程竞争资源,可能会造成死锁;被阻塞的线程可能优先级很高却一直无法获取锁。

因此实现原子操作可使用CAS指令。

CAS

CAS 即Compare and Swap,比较并交换。每个CAS操作过程都包括三个运算符:
内存地址V、期望值A、新的值B
如果操作的时候地址V上的值等于期望的值A,则将地址V上的值更新为B,否则不做任何操作,但要返回原值。循环CAS就是不断的进行CAS操作直到操作成功。可防止内存中共享变量出现脏读脏写的问题。
CAS是通过硬件CPU和内存,利用CPU的多处理能力实现硬件层面的阻塞,再加上volatile变量的特性来实现基于原子操作的线程安全。

(1)get变量值(旧值)----->
(2)计算后得到新值------>
(3)比较内存中变量值和旧值----->
(4)如果(3)相等,则让新值替换旧值,否则继续(1)

CAS实现原子操作的三大问题

ABA问题

由于CAS在操作值的时候需要检查旧值是否发生变化,如果未变化则更新值,但如果一个值原来是A,变成了B,然后又变为了A,这样虽然值已经发生了变化,但使用CAS进行检查时会发现它的旧值“未变化”。
而要解决该问题,就是使用版本号。即给变量添加版本号,每次更新变量都去更新它的版本号,如刚刚A->B->A,便变成了 1A->2B->3A。就像在家里倒了一杯水,你还没来得及喝,但是被家人喝掉, 然后又给你接满了一杯水,这样如果在不知道的情况下,你会认为这仍然是之前那杯水。又或者你的资金被别人挪用了,然后又还给了你,虽然钱没有变,但造成的问题时那个人已经犯罪了。

循环时间长,开销大

如果CAS一直处于自旋不成功状态,CPU的开销会大大增加。

只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,可使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,当然可以用锁。
也可以把多个共享变量合并成一个共享变量来操作。如,有两个共享变量 i=a,j=b,合并一下 ij=ab,然后用 CAS 来操作 ij,然后通过AtomicReference 类来保证引用对象之间的原子性,便实现了多个变量放在一个对象里来进行 CAS 操作。

JDK中相关原子操作类的使用

概览

更新基本类型类:AtomicBooleanAtomicIntegerAtomicLong
更新数组类:AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray
更新引用类型AtomicReferenceAtomicMarkableReferenceAtomicStampedReference
原子更新字段类: AtomicReferenceFieldUpdaterAtomicIntegerFieldUpdaterAtomicLongFieldUpdater

基本数据类型的原子操作类以AtomicInteger为例,其余差不多。

AtomicInteger

常用方法如下:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
 
    //获取指针类Unsafe
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //内存偏移量
    private static final long valueOffset;
 
    static {
        try {
           //通过objectFieldOffset()方法,获取对象属性的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
   	//当前的变量值
    private volatile int value;
 
    public AtomicInteger(int initialValue) {
    	//初始化变量值
        value = initialValue;
    }
    //value初始化为0
    public AtomicInteger() {
    }
   	//返回当前值
    public final int get() {
        return value;
    }
    //设置当前值
    public final void set(int newValue) {
        value = newValue;
    }
    //懒设置,使用Unsafe.putOrderedObject方法实现,实现非堵塞的写入,最终会将value置为newValue。
    //该方法可能出现其他线程在未来很小的一段时间内无法获取到新值,但可获取到旧值
    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }
   //以原子方式设置为给定值并返回旧值。
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
    //以下是getAndSetInt源码,通过while循环重试更新值,直到成功,也是通过CAS实现(JDK8版本)
     /*public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));

        return var5;
    }*/
   //原子更新value,如果当前值等于expect,则设置为update
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    //以原子方式将当前值增加一
    //返回旧值,底层同getAndSetInt一样使用CAS操作
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    //以原子方式将当前值减一
    //返回旧值,CAS操作
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }
   //以原子方式将给定值delta添加到当前值
   //返回旧值,CAS操作
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    //以原子方式将当前值加1,返回新值
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    //当前值减1,返回新值
    public final int decrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }
   //以原子方式将当前值增加delta,返回新值
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }
    //使用给定函数的结果以原子方式更新当前值,返回更新后的值。给的函数是无副作用的,因为当尝试更新由于线程之间的争用而失败时,它可能会重新应用
     public final int updateAndGet(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return next;
    }
    //同上,使用给定函数的结果以原子方式更新当前值,返回更新前的值。应用该函数时,将当前值作为其第一个参数,并将给定的update作为第二个参数。
	 public final int getAndAccumulate(int x,
                                      IntBinaryOperator accumulatorFunction) {
        int prev, next;
        do {
            prev = get();
            next = accumulatorFunction.applyAsInt(prev, x);
        } while (!compareAndSet(prev, next));
        return prev;
    }
    //同getAndAccumulate,但返回更新后的值
     public final int accumulateAndGet(int x,
                                      IntBinaryOperator accumulatorFunction) {
        int prev, next;
        do {
            prev = get();
            next = accumulatorFunction.applyAsInt(prev, x);
        } while (!compareAndSet(prev, next));
        return next;
    }
}

使用演示:

public class UseAtomicInt {
    static AtomicInteger atomicInteger = new AtomicInteger(10);

    public static void main(String[] args) {
        //返回旧值10
        System.out.println(atomicInteger.getAndIncrement());
        //返回新值12
        System.out.println(atomicInteger.incrementAndGet());
        atomicInteger.compareAndSet(12,1);
        //加24 返回25
        System.out.println(atomicInteger.addAndGet(24));
        //自定义函数
        IntBinarOperImpl intBinarOper=new IntBinarOperImpl();
        //传入自定义函数,传入更新值
        atomicInteger.accumulateAndGet(1, intBinarOper);
        //返回26
        System.out.println(atomicInteger.get());
    }
}
//自定义函数,实现IntBinaryOperator接口
public class IntBinarOperImpl implements IntBinaryOperator {
    @Override
    public int applyAsInt(int left, int right) {
        //简单定义原来的数加上要更新的数
        return left+right;
    }
}

AtomicIntegerArray

是提供原子的方式更新数组里的整型,还包括高级原子操作。常用方法如下:

public class AtomicIntegerArray implements java.io.Serializable {
    private static final long serialVersionUID = 2862133569453604235L;
	//同上
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //获取数组第一个元素的偏移地址
    private static final int base = unsafe.arrayBaseOffset(int[].class);
    //存储移位个数
    private static final int shift;
    //保存的数组
    private final int[] array;

    static {
    	//获取该类型的数组中元素的大小(字节)。
        int scale = unsafe.arrayIndexScale(int[].class);
        //scale如果不是2的次幂则抛出异常
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
         //计算得到需要移位的个数,即scale的次幂数
        shift = 31 - Integer.numberOfLeadingZeros(scale);
    }
    //返回第i个元素的地址
 	private static long byteOffset(int i) {
 		//shift为偏移位数,base为第一个元素的地址,i移shift个位后+基位置得到第i个元素位置
        return ((long) i << shift) + base;
    }
    //先检测i是否越界,再调用byteOffset(i)返回
    private long checkedByteOffset(int i) {
        if (i < 0 || i >= array.length)
            throw new IndexOutOfBoundsException("index " + i);
        return byteOffset(i);
    }
    //构造初始化给定长度数组
    public AtomicIntegerArray(int length) {
        array = new int[length];
    }	
    //创建一个新AtomicIntegerArray,其长度与传入数组相同,并从给定数组中复制所有元素。所以当 AtomicIntegerArray 对内部的数组元素进行修改时,不会影响传入的数组。
    public AtomicIntegerArray(int[] array) {
        this.array = array.clone();
    }
	//返回当前位置的元素
    public final int get(int i) {
    	//调用checkedByteOffset获取当前位置的元素地址
        return getRaw(checkedByteOffset(i));
    }
	//getIntVolatile方法获取数组中offset偏移地址对应的整型field的值,支持volatile load语义。返回该值。
   private int getRaw(long offset) {
        return unsafe.getIntVolatile(array, offset);
    }
    //更新第i个位置的值,也需先计算当前位置的元素的offset
    public final void set(int i, int newValue) {
        unsafe.putIntVolatile(array, checkedByteOffset(i), newValue);
    }
    //同AtomicInteger的lazySet,这里更新第i个位置的元素
    public final void lazySet(int i, int newValue) {
        unsafe.putOrderedInt(array, checkedByteOffset(i), newValue);
    }

    public final int getAndSet(int i, int newValue) {
        return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue);
    }
    
	//如果当前值等于预期值,则以原子方式将数组位置 i 的元素设置成 update 值。
    public final boolean compareAndSet(int i, int expect, int update) {
        return compareAndSetRaw(checkedByteOffset(i), expect, update);
    }

    private boolean compareAndSetRaw(long offset, int expect, int update) {
        return unsafe.compareAndSwapInt(array, offset, expect, update);
    }
	//原子的方式让第i个元素加1
    public final int getAndIncrement(int i) {
        return getAndAdd(i, 1);
    }

  	//原子的方式让第i个元素减1
    public final int getAndDecrement(int i) {
        return getAndAdd(i, -1);
    }

   	//原子的方式让第i个元素加delta
    public final int getAndAdd(int i, int delta) {
        return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
    }
	//还有其他一些方法都和Integer原子类基本一样

简单使用:

public class AtomicArray {
    static int[] value = new int[] { 1, 2,3 };
    static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(value);
    public static void main(String[] args) {
        //返回第一个位置的元素  2
        System.out.println(atomicIntegerArray.get(1));
        //更新第1个位置的元素
        atomicIntegerArray.set(1,4);
        //返回4
        System.out.println(atomicIntegerArray.get(1));
        //设置第0个位置为3,返回更新前的值 1
        System.out.println( atomicIntegerArray.getAndSet(0, 3));
        //返回更新后的3
        System.out.println(atomicIntegerArray.get(0));
        //返回1,原数组不会变化
        System.out.println(value[0]);

        atomicIntegerArray.compareAndSet(0,3,4);
        System.out.println(atomicIntegerArray.get(0));
    }
}

更新引用类型

原子更新基本类型的 AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic 包提供了以下 3类。
AtomicReferenceAtomicStampedReferenceAtomicMarkableReference

AtomicReference

方法同前面几个类基本一样,但支持泛型,可传入对象,支持对对象的原子操作

AtomicStampedReference和AtomicMarkableReference

AtomicStampedReference 使用版本戳的记录每次改变后的版本号,这样的话就不会存在 ABA问题了。
AtomicMarkableReferenceAtomicStampedReference 基本相同,AtomicStampedReferencePair类 中使用 int类型的stamp 作为计数器,每次操作都更新版本,关注的的是动过几次。
AtomicMarkableReferencePair类 使用的是 boolean 类型的mark,关注的的是有没有被动过。

AtomicStampedReference源码如下:

  1. 首先定义了一个Pair内部类:

     private static class Pair<T> {
            final T reference;
            final int stamp;
            private Pair(T reference, int stamp) {
                this.reference = reference;
                this.stamp = stamp;
            }
            static <T> Pair<T> of(T reference, int stamp) {
                return new Pair<T>(reference, stamp);
            }
        }
    

    该类将元素的值(reference)和版本号(stamp)都维护在自己身上。

  2. 成员变量

    //声明Pair类型变量
    private volatile Pair<V> pair;
    //获取Unsafe
    private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
    //使用unsafe获取偏移量保存到pairOffset
    private static final long pairOffset =
            objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
    
      static long objectFieldOffset(sun.misc.Unsafe UNSAFE,
                                  String field, Class<?> klazz) {
        try {
            return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));
        } catch (NoSuchFieldException e) {
            // Convert Exception to corresponding Error
            NoSuchFieldError error = new NoSuchFieldError(field);
            error.initCause(e);
            throw error;
                
    //后面两个放在源码的最后,不太好找	
    
  3. 构造方法

     public AtomicStampedReference(V initialRef, int initialStamp) {
            pair = Pair.of(initialRef, initialStamp);
        }
    

    传入初始值和初值版本戳保存到pair。

  4. 方法
    4.1 getReference()getStamp():返回元素值和返回版本戳

    public V getReference() {
            return pair.reference;
    }
     public int getStamp() {
        return pair.stamp;
    }
    

    4.2 get()

    //返回当前元素的值,并将当前元素的版本号保存到传入的数组中的第一个位置
    public V get(int[] stampHolder) {
            Pair<V> pair = this.pair;
            stampHolder[0] = pair.stamp;
            return pair.reference;
        }
    

    4.3 casPair()

    //cmp:期望的pair,val:新的pair
    //CAS更新当前的pair
    private boolean casPair(Pair<V> cmp, Pair<V> val) {
            return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
     }
    

    4.4 compareAndSet()

    /**
     * 原子的修改元素的值和版本戳
     * @param 期望值
     * @param 新值
     * @param 期望版本戳
     * @param 新版本戳
     */
     public boolean compareAndSet(V   expectedReference,
                                     V   newReference,
                                     int expectedStamp,
                                     int newStamp) {
            Pair<V> current = pair;
            return
                expectedReference == current.reference &&
                expectedStamp == current.stamp &&
                ((newReference == current.reference &&
                  newStamp == current.stamp) ||
                 casPair(current, Pair.of(newReference, newStamp)));
        }
    

    该方法首先判断期望的值和版本戳是否和当前的值和版本戳相同,如果不相同则直接返回false,如果相同则继续比较新的值和版本戳是否和当前值相同,如果相同则直接返回true,否则对当前的pair进行CAS操作。
    4.5 weakCompareAndSet()

    // 同compareAndSet,但可能不能保证原子性
    public boolean weakCompareAndSet(V   expectedReference,
                                         V   newReference,
                                         int expectedStamp,
                                         int newStamp) {
            return compareAndSet(expectedReference, newReference,
                                 expectedStamp, newStamp);
        }
    

    其代码同compareAndSet无差别,但在JDK9中,两个方法都添加了@HotSpotIntrinsicCandidate注解,使用该注解的方法在JVM中都有一套基于CPU指令的高效的实现,且会在运行时会替代JDK的源码实现,从而获得更高的效率。即HotSpot可能会手动实现这个方法。参考https://blog.csdn.net/lzcaqde/article/details/80868854的解读。
    4.6set()

    /**
     * 源码解释无条件更新当前元素的值和版本戳
     * @param newReference 新值
     * @param newStamp 新版本戳
     */
    public void set(V newReference, int newStamp) {
            Pair<V> current = pair;
            if (newReference != current.reference || newStamp != current.stamp)
                this.pair = Pair.of(newReference, newStamp);
        }
    

    4.6attemptStamp()

    如果引用对象为期望值,则重新设置新的版本戳。

    
     public boolean attemptStamp(V expectedReference, int newStamp) {
            Pair<V> current = pair;
            return
                expectedReference == current.reference &&
                (newStamp == current.stamp ||
                 casPair(current, Pair.of(expectedReference, newStamp)));
        }
    

示例:

public class UseAtomicStampedReference {
    /**
     * 设置初始值和初始版本戳
     */
    static AtomicStampedReference<String> asr
            = new AtomicStampedReference("wml",0);

    public static void main(String[] args) throws InterruptedException {
        //拿到当前的版本号(旧)
        final int oldStamp = asr.getStamp();
        final String oldReference = asr.getReference();
        System.out.println("旧值:"+oldReference+",旧版本戳:"+oldStamp);

        Thread rightStampThread = new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+":当前变量值:"
                    +oldReference + ",当前版本戳:" + oldStamp );
            //更新引用对象值和版本戳
            asr.compareAndSet(oldReference, "wml222", oldStamp, oldStamp + 1);
            int[] stampHolder=new int[2];
            stampHolder[0]=0;
            //调用get,返回当前值,并将版本保存到stampHolder[0]中
            asr.get(stampHolder);
            //打印当前版本
            System.out.println("使用get()获取版本:"+stampHolder[0]);
        });

        Thread errorStampThread = new Thread(() -> {
            String reference = asr.getReference();
            System.out.println(Thread.currentThread().getName()+":当前变量值:"
                    +reference + ",当前版本戳:" + asr.getStamp());
            //更新引用对象值和版本戳
            asr.compareAndSet(reference, "wml333", oldStamp, oldStamp + 1);

        });
        rightStampThread.start();
        rightStampThread.join();
        errorStampThread.start();
        errorStampThread.join();

        System.out.println("Main线程:当前变量值:"
                +asr.getReference() + ",当前版本戳:" + asr.getStamp());

        asr.set("set新值",3);
        System.out.println("使用set更新:当前变量值:"
                +asr.getReference() + ",当前版本戳:" + asr.getStamp());
    }
}

线程errorStampThread没有使用更新后的版本戳作为期望版本,因此该线程CAS失败。

参考:
https://zhuanlan.zhihu.com/p/65240318

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!