CAS的理解.md

1.概述

CAS,英文全称为Compare And Set,Java并发中存在乐观锁和悲观锁,而CAS就是乐观锁的一种

实现过程:我们都知道Java内存模型规定了主内存和工作内存,每个线程都在自己的工作内存中进行数据的修改,并最终写回到主内存中,那么多个线程同时修改主内存的数据时,如何保证线程安全性呢?我们以往的做法是使用锁,一般使用synchronized关键字,这也是一种悲观锁,那么作为乐观锁的一种,CAS是怎样保证线程安全的呢?

我们来看下面的代码:

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        // 获取真实值,并替换为相应的值
        boolean b = atomicInteger.compareAndSet(5, 2019);
        System.out.println(b); // true
        boolean b1 = atomicInteger.compareAndSet(5, 2020);
        System.out.println(b1); // false
        atomicInteger.getAndIncrement();
    }
}

首先创建一个AtomicInteger对象,并设置初始值为5,然后使用AtomicInteger对象进行赋值操作,atomicInteger.compareAndSet(5,2019),既然属于乐观锁,那么这个线程就不会上锁,乐观的以为不会发生线程安全问题,进行赋值的时候,将主内存中的值和期望值进行比较,如果相等,那么就认为没有其他线程进行修改,直接将update的值赋给主内存,但是如果主内存中的值和期望值不相等,那么说明有其他线程为非作歹,取消交易,不进行赋值操作,重新读取主内存中的值。

多线程情况下就尽量不要使用i++,这个操作不能保证原子性,可以使用AtomicInteger替代,可以使用AtomicInteger对象调用getAndIncrement()方法,

2.CAS底层原理

上面提到,在多线程中使用atomicInteger.getAndIncrement()来替代i++操作,可以保证其原子性,来看getAndIncrement()源码:

/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

底层调用了unsafe类的getAndInt()方法,那么Unsafe类是什么?

源码:

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 {
            // 获取下面 value 的地址偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
	// ...
}

Unsafe类是CAS实现的一个核心类,由于Java方法无法直接访问底层操作系统,而Unsafe类中含有大量的native方法:

	//...
	public native long staticFieldOffset(Field var1);

    public native long objectFieldOffset(Field var1);

    public native Object staticFieldBase(Field var1);

    public native boolean shouldBeInitialized(Class<?> var1);

    public native void ensureClassInitialized(Class<?> var1);
	//...

native方法通常在Java代码中没有显示实现,该方法的实现由非Java语言实现,native方法也就是Java和底层操作系统之间的一个接口,所以Unsafe类就相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe存在于sun.misc包中,其内部的native方法可以像C指针一样直接操作内存。

变量 vauleOffset,表示该变量值在内存中的偏移量,因为 Unsafe 就是根据内存偏移量来获取数据的。

变量 value 用 volatile 修饰,保证了多线程之间的内存可见性。


继续查看getAndAddInt()方法源码:

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;
    }

原语:

上面源码中的getIntVolatile,compareAndSwapInt方法都是native方法,其都是依赖于操作系统的原语实现,原语由若干条指令构成,原语的执行顺序是连续的,不能被打断

这个代码的实现使用了自旋锁的方法,首先,this表示调用当前方法的对象,也也就是atomicInteger对象,

var5 = this.getIntVolatile(var1, var2);这个语句中,getIntVolatile()方法已经是native方法,

public native int getIntVolatile(Object var1, long var2);,这个方法因为使用了volatile关键字,这样可以保证可见性,所以直接从主内存中读取最新的原始值,循环判断条件为while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

这条语句的含义为:根据var1对象中偏移量为var2的值,也就是当前线程工作内存中的值,也就是上面所说的期望值,和当前主内存中的最新值var5进行比较,如果相等,跳出while循环,并进行var5 + var4的赋值操作,如果不相等,一直循环下去。

3.CAS的缺点

  • 使用自旋锁的方法,如果CAS失败,那么会一直进行while循环,增加CPU的开销
  • 只能保证一个共享变量的原子性操作
  • ABA问题

4.ABA问题

原子引用

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User cuzz = new User("cuzz", 18);
        User faker = new User("faker", 20);
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(cuzz);
        System.out.println(atomicReference.compareAndSet(cuzz, faker)); // true
        System.out.println(atomicReference.get()); // User(userName=faker, age=20)
    }
}

既然Integer对象有原子类,那么其他对象是不是也能实现呢?这就依赖于原子引用AtomicReference,


ABA问题的产生

public class ABADemo {
    private static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);

    public static void main(String[] args) {
        new Thread(() -> {
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }).start();

        new Thread(() -> {
            // 保证上面线程先执行
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicReference.compareAndSet(100, 2019);
            System.out.println(atomicReference.get()); // 2019
        }).start();
    }
}

尽管第二个线程最终可以进行赋值成功,他自己以为没有发生线程安全问题,但是第一个线程对100先是更改为101,又更改回了100,说明在第二线程之前已经有另外一个线程对主存中的数据的进行改动,线程二的盲目乐观是不行的。

时间戳原子引用

public class ABADemo2 {
    private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + " 的版本号为:" + stamp);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1 );
            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1 );
        }).start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + " 的版本号为:" + stamp);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
            System.out.println(b); // false
            System.out.println(atomicStampedReference.getReference()); // 100
        }).start();
    }
}

通过使用时间戳也就是版本号的方法,来保证主内存中数据的纯洁性,这样假如当前线程执行时即使期望值和原始值相等,但是时间戳不能匹配,说明已经有人抢先一步修改了主存内容,还是不能进行CAS。