java乐观锁之CAS原理解析

目录

  • 含义
  • 原理分析
  • CPU原语
  • CAS缺陷
    • ABA问题
    • 循环时间长开销大
    • 只能对单个共享变量保证原子性操作

含义

CAS(CompareAndSwap) 即比较并替换,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。

乐观锁的含义就是假设没有发生冲突,那么我正好可以进行某项操作,如果要是发生冲突呢,那我就重试直到成功,乐观锁最常见的就是CAS。
无论是ReenterLock内部的AQS,还是各种Atomic开头的原子类,内部都应用到了CAS

原理分析

以java.util.concurrent.atomic.***AtomicInteger***为例进行分析

/**
 * An {@code int} value that may be updated atomically.  See the
 * {@link java.util.concurrent.atomic} package specification for
 * description of the properties of atomic variables. An
 * {@code AtomicInteger} is used in applications such as atomically
 * incremented counters, and cannot be used as a replacement for an
 * {@link java.lang.Integer}. However, this class does extend
 * {@code Number} to allow uniform access by tools and utilities that
 * deal with numerically-based classes.
 *
 * @since 1.5
 * @author Doug Lea
*/
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
     /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    ...
    }
  • Unsafe是位于sun.misc包下的一个类,Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。

在jdk1.9中,对Usafe进行了删除,因此那些基于Usafe开发的框架慢慢的都死掉了。

  • valueOffset是long类型的,此处代表的含义就是对象成员变量value相对对象内存地址的偏移量

valueOffset的赋值是放在static代码块中,也就是说类加载初始化的时候就执行一次,从而获取了value成员变量相对于当前对象内存地址的偏移量

  • value变量使用volatile修饰保证了内存可见性,为后面的CAS提供了可能性。其实实际存储的值是放在value中的
  • incrementAndGet方法剖析
    调用链:
    incrementAndGet() → unsafe.getAndAddInt(this, valueOffset, 1) + 1 → getAndAddInt(Object var1, long var2, int var4)
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;
    }

var1: 对应当前对象this; var2对应偏移量valueOffset;var4对应增量 1;
所以上面代码等价于

public final int getAndAddInt(Object this,long valueOffset,int 1){
    int var5;
    do {
        var5 = this.getIntVolatile(this, valueOffset);
    } while(!this.compareAndSwapInt(this, valueOffset, var5, var5 + 1));
    return var5;
    }

通过上面的变量替换,只剩下var5了。

var5 = this.getIntVolatile(this, valueOffset);

这是个native方法,其实就是获取var1中,var2偏移量处的值。var1就是AtomicInteger,var2就是我们前面提到的valueOffset,这样我们就从内存里获取到现在valueOffset处的值了,也就是获取到当前value的值

public final int getAndAddInt(Object this,long valueOffset,int 1){
    int expect;
    do {
        expect = this.getIntVolatile(this, valueOffset);
    } while(!this.compareAndSwapInt(this, valueOffset, expect, update));
    return expect;
    }

getAndAddInt方法执行逻辑:

  1. 通过getIntVolatile(this,valueOffset)获取到当前value的值,作为expect(期望值)。
  2. compareAndSwapInt方法含义:
    如果this内的value和expect相等,就证明没有其他线程改变过这个变量,那么就更新它为update,如果这一步的CAS没有成功,那就采用自旋的方式(无限循环)继续进行CAS操作

CPU原语

CAS,比较并交换。乍一看这也是两个步骤了啊,其实在JNI里是借助于一个CPU指令(cmpxchgl)完成的。所以还是原子操作。

CAS缺陷

ABA问题

  • 问题描述

CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是CAS的ABA问题。

  • 解决办法

常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
目前在JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  • AtomicStampedReference原理解析

AtomicStampedReference就是加了版本号的AtomicReference。
可以看到,除了传入一个初始的引用变量initialRef外,还有一个initialStamp变量,initialStamp其实就是版本号(或者说时间戳),用来唯一标识引用变量。

源码如下:

/**
 * An {@code AtomicStampedReference} maintains an object reference
 * along with an integer "stamp", that can be updated atomically.
 *
 * 

Implementation note: This implementation maintains stamped * references by creating internal objects representing "boxed" * [reference, integer] pairs. * * @since 1.5 * @author Doug Lea * @param The type of object referred to by this reference */ public class AtomicStampedReference<V> { 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); } } private volatile Pair<V> pair; /** * Creates a new {@code AtomicStampedReference} with the given * initial values. * * @param initialRef the initial reference * @param initialStamp the initial stamp */ public AtomicStampedReference(V initialRef, int initialStamp) { pair = Pair.of(initialRef, initialStamp); } /** * Returns the current value of the reference. * * @return the current value of the reference */ public V getReference() { return pair.reference; } /** * Returns the current value of the stamp. * * @return the current value of the stamp */ public int getStamp() { return pair.stamp; } ... /** * Atomically sets the value of both the reference and stamp * to the given update values if the * current reference is {@code ==} to the expected reference * and the current stamp is equal to the expected stamp. * * @param expectedReference the expected value of the reference * @param newReference the new value for the reference * @param expectedStamp the expected value of the stamp * @param newStamp the new value for the stamp * @return {@code true} if successful */ 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))); } ... // Unsafe mechanics private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe(); private static final long pairOffset = objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class); private boolean casPair(Pair<V> cmp, Pair<V> val) { return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); } 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; } } }

  • 实例:
AtomicStampedReference atomicStampedReference = new AtomicStampedReference(new Integer(100), 1);
        List<Thread> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            Thread thread = new Thread(() -> {
                boolean flag = false;
                do {
                    int stamp = atomicStampedReference.getStamp();
                    flag = atomicStampedReference.compareAndSet((Integer) atomicStampedReference.getReference(), Integer.valueOf((Integer) ((Integer) atomicStampedReference.getReference()).intValue() + 1), stamp, stamp + 1);
                } while (!flag);
            });
            list.add(thread);
            thread.start();
        }
        list.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println((Integer) atomicStampedReference.getReference()); // 1100

循环时间长开销大

上面我们说过如果CAS不成功,则会原地自旋,如果长时间自旋会给CPU带来非常大的执行开销。

只能对单个共享变量保证原子性操作

这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

  • AtomicReference原理分析
    AtomicReference就是以原子方式更新对象引用。
    源码如下
/**
 * An object reference that may be updated atomically. See the {@link
 * java.util.concurrent.atomic} package specification for description
 * of the properties of atomic variables.
 * @since 1.5
 * @author Doug Lea
 * @param  The type of object referred to by this reference
 */
public class AtomicReference<V> implements java.io.Serializable {
    private static final long serialVersionUID = -1848883965231344442L;

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicReference.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile V value;

    /**
     * Creates a new AtomicReference with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicReference(V initialValue) {
        value = initialValue;
    }

    /**
     * Creates a new AtomicReference with null initial value.
     */
    public AtomicReference() {
    }
    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }
   /**
     * Atomically updates the current value with the results of
     * applying the given function, returning the updated value. The
     * function should be side-effect-free, since it may be re-applied
     * when attempted updates fail due to contention among threads.
     *
     * @param updateFunction a side-effect-free function
     * @return the updated value
     * @since 1.8
     */  
 public final V updateAndGet(UnaryOperator<V> updateFunction) {
        V prev, next;
        do {
            prev = get();
            next = updateFunction.apply(prev);
        } while (!compareAndSet(prev, next));
        return next;
    }
    ...
    }

通过上述源码可以看到,AtomicReference使用泛型定义。操作的是对象引用,而不是具体某个变量(AtomicInteger等),从而可以操作共享资源对象,而不单单是共享变量。(具体原理可以参考上面AtomicInteger的原理解析,大同小异,只是把int类型的value换成了对象引用V)

  • 实例
AtomicReference<Integer> ref = new AtomicReference<>(new Integer(1000));
        List<Thread> list = new ArrayList<>();
        UnaryOperator<Integer> integerUnaryOperator = x -> x + 1;
        for(int i=0;i<1000;i++){
             Thread thread = new Thread( ()->{ref.updateAndGet(integerUnaryOperator);});
            list.add(thread);
            thread.start();
        }
        list.forEach( t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(ref.get()); // 2000

你可能感兴趣的:((多)线程)