说到CAS(CompareAndSwap),不得不先说一说悲观锁和乐观锁,由于CAS是乐观锁思想的一种实现。java
悲观锁:老是很悲观的认为,每次拿数据都会有其余线程并发执行,因此每次都会进行加锁,用完以后释放锁,其余的线程才能拿到锁,进而拿到资源进行操做。java中的synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。安全
乐观锁:老是很乐观认为,本身拿到数据操做的时候,没有其余线程来并发操做,等本身操做结束要更新数据时,判断本身对数据操做的期间有没有其余线程进行操做,若是有,则进行重试,直到操做变动成功。乐观锁常使用CAS和版本号机制来实现。java中java.util.atomic包下的原子类都是基于CAS实现的。多线程
CAS指CompareAndSwap,顾名思义,先比较后交换。比较什么?交换什么呢?并发
CAS中有三个变量:内存地址V,期待值A, 更新值B。ide
当且仅当内存地址V对应的值与期待值A时相等时,将内存地址V对应的值更换为B。函数
有了悲观锁,乐观锁的知识,让咱们走进java.util.atomic包,看一看java中CAS的实现。测试
这就是java.util.atomic包下的类,咱们着重看AtomicInteger源码(其余的都是同样的思想实现的)this
而后思考CAS有什么弊端?如何解决弊端?有什么优缺点?atom
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // 使用Unsafe.compareAndSwapInt进行原子更新操做 private static final Unsafe unsafe = Unsafe.getUnsafe(); //value对应的存储地址偏移量 private static final long valueOffset; static { try { //使用反射及unsafe.objectFieldOffset拿到value字段的内存地址偏移量,这个值是固定不变的 valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } //volatile修饰的共享变量 private volatile int value; //.......... }
上面的代码其实就是为了初始化内存值对应的内存地址偏移量valueOffset,方便后续执行CAS操做时使用。由于这个值一旦初始化,就不会更改,因此使用static final 修饰。线程
咱们能够看到value使用了volatile修饰,其中也说了volatile的语义。
咱们都知道若是进行value++操做,并发下是不安全的。上一篇中咱们也经过例子证实了volatile只能保证可见性,不能保证原子性。由于value++自己不是原子操做,value++分了三步,先拿到value的值,进行+1,再赋值回value。
咱们先看一看AtomicInteger提供的CAS操做。
/** * 原子地将value设置为update,若是valueOffset对应的值与expect相等时 * * @param expect 期待值 * @param update 更新值 * @return 若是更新成功,返回true;在valueOffset对应的值与expect不相等时返回false */ public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
咱们已经知道CAS的原理,那来看看下面的测试。你知道输出的结果是多少吗?评论区给出你的答案吧。
public class AtomicIntegerTest { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.compareAndSet(0, 1); atomicInteger.compareAndSet(2, 1); atomicInteger.compareAndSet(1, 3); atomicInteger.compareAndSet(2, 4); System.out.println(atomicInteger.get()); } }
Unsafe提供了三个原子更新的方法。
关于Unsafe类,由于java不支持直接操做底层硬件资源,如分配内存等。若是你使用unsafe开辟的内存,是不被JVM垃圾回收管理,须要本身管理,容易形成内存泄漏等。
咱们上面说了,value++不是原子操做,不能在并发下使用。咱们来看看AtomicInteger提供的原子++操做。
/** * 原子地对value进行+1操做 * * @return 返回更新后的值 */ public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } /** * unsafe提供的方法 * var1 更改的目标对象 * var2 目标对象的共享字段对应的内存地址偏移量valueOffset * var4 须要在原value上增长的值 * @return 返回未更新前的值 */ public final int getAndAddInt(Object var1, long var2, int var4) { //期待值 int var5; do { //获取valueOffset对应的value的值,支持volatile load var5 = this.getIntVolatile(var1, var2); //若是原子更新失败,则一直重试,直到成功。 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
咱们看到CAS只能原子的更新一个值,若是咱们要原子更新多个值,CAS能够作到吗?答案是能够的。
若是要原子地更新多个值,就须要使用AtomicReference。其使用的是compareAndSwapObject方法。能够将多个值封装到一个对象中,原子地更换对象来实现原子更新多个值。
public class MultiValue { private int value1; private long value2; private Integer value3; public MultiValue(int value1, long value2, Integer value3) { this.value1 = value1; this.value2 = value2; this.value3 = value3; } } public class AtomicReferenceTest { public static void main(String[] args) { MultiValue multiValue1 = new MultiValue(1, 1, 1); MultiValue multiValue2 = new MultiValue(2, 2, 2); MultiValue multiValue3 = new MultiValue(3, 3, 3); AtomicReference<MultiValue> atomicReference = new AtomicReference<>(); //由于构造AtomicReference时,没有使用有参构造函数,因此value默认值是null atomicReference.compareAndSet(null, multiValue1); System.out.println(atomicReference.get()); atomicReference.compareAndSet(multiValue1, multiValue2); System.out.println(atomicReference.get()); atomicReference.compareAndSet(multiValue2, multiValue3); System.out.println(atomicReference.get()); } } //输出结果 //MultiValue{value1=1, value2=1, value3=1} //MultiValue{value1=2, value2=2, value3=2} //MultiValue{value1=3, value2=3, value3=3}
咱们再看一看AtomicReference的compareAndSet方法。
注意:这里的比较都是使用==而非equals方法。因此最好封装的MultiValue不要提供set方法。
public final boolean compareAndSet(V expect, V update) { return unsafe.compareAndSwapObject(this, valueOffset, expect, update); }
假设你的帐户上有100块钱,你要给女票转50块钱。
咱们使用CAS进行原子更新帐户余额。因为某种缘由,你第一次点击转帐出现错误,你觉得没有发起转帐请求,这时候你又点击了一次。系统开启了两个线程进行转帐操做,第一个线程进行CAS比较,发现你的帐户上预期是100块钱,实际也有100块钱,这时候转走了50,须要设置为100 - 50 = 50 元,这时帐户余额为50
第一个线程操做成功了,第二个线程因为某种缘由阻塞住了;这时候,你的家人又给你转了50块钱,而且转帐成功。那你帐户上如今又是100块钱;
太巧了,第二个线程被唤醒了,发现你的帐户是100块钱,跟预期的100是相等的,这时候又CAS为50。大兄弟,哭惨了,你算算,正确的场景你要有多少钱?这就是CAS存在的ABA问题。
public class AtomicIntegerABA { private static AtomicInteger atomicInteger = new AtomicInteger(100); public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(3); //线程1 executorService.execute(() -> { System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get()); atomicInteger.compareAndSet(100, 50); System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get()); }); //线程2 executorService.execute(() -> { try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get()); atomicInteger.compareAndSet(50, 100); System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get()); }); //线程3 executorService.execute(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get()); atomicInteger.compareAndSet(100, 50); System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get()); }); executorService.shutdown(); } } //输出结果 //pool-1-thread-1 - 100 //pool-1-thread-1 - 50 //pool-1-thread-2 - 50 //pool-1-thread-2 - 100 //pool-1-thread-3 - 100 //pool-1-thread-3 - 50
你们心想,靠,这不是坑吗?那还用。。。。。。。。。。。。。。冷静,冷静。你能想到的问题,jdk都能想到。atomic包提供了一个AtomicStampedReference
看名字是否是跟AtomicReference很像啊,其实就是在AtomicReference上加上了一个版本号,每次操做都对版本号进行自增,那每次CAS不只要比较value,还要比较stamp,当且仅当二者都相等,才可以进行更新。
public AtomicStampedReference(V initialRef, int initialStamp) { pair = Pair.of(initialRef, initialStamp); } //定义了内部静态内部类Pair,将构造函数初始化的值与版本号构造一个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); } } //因此咱们以前的value就对应为如今的pair private volatile Pair<V> pair;
让咱们来看一看它的CAS方法。
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))); } private boolean casPair(Pair<V> cmp, Pair<V> val) { return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); }
仍是上面转帐的例子,咱们使用AtomicStampedReference来看看是否解决了呢。
public class AtomicStampedReferenceABA { /** * 初始化帐户中有100块钱,版本号对应0 */ private static AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(100, 0); public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(3); int[] result = new int[1]; //线程1 executorService.execute(() -> { System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result)); //将100更新为50,版本号+1 atomicInteger.compareAndSet(100, 50, 0, 1); System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result)); }); //线程2 executorService.execute(() -> { try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result)); //将50更新为100,版本号+1 atomicInteger.compareAndSet(50, 100, 1, 2); System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result)); }); //线程3 executorService.execute(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result)); //此线程仍是觉得没有其余线程进行过更改,因此旧版本号仍是0 atomicInteger.compareAndSet(100, 50, 0, 1); System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result)); }); executorService.shutdown(); } } //输出结果 //pool-1-thread-1 - 100 //pool-1-thread-1 - 50 //pool-1-thread-2 - 50 //pool-1-thread-2 - 100 //pool-1-thread-3 - 100 //pool-1-thread-3 - 100
妈妈不再用担忧个人钱少了。
本篇详细讲解了CAS的原理,CAS能够进行原子更新一个值(包括对象),主要用于读多写少的场景,如原子自增操做,若是多线程调用,在CAS失败以后,会死循环一直重试,直到更新成功。这种状况是很耗CPU资源的,虽然没有锁,但循环的自旋可能比锁的代价还高。同时存在ABA问题,但AtomicStampedReference经过加入版本号机制已经解决。其实对于atomic包,jdk1.8新增的LongAdder,效率比AtomicLong高,9龙还未涉足,之后确定会品一品。J.U.C(java.util.concurrent)包中大量使用了CAS,ConcurrentHashMap也使用到,若是不了解CAS,怎么入手J.U.C呢。