Java CAS

Java CAS 1. 概念及基本原理CAS compare and swap 的缩写中文翻译成 比较并交换 , 是实现并发算法时常用到的一种技术。它包含三个操作数——内存位置、预期原值 及 更新值。执行 CAS 操作的时候将内存位置的值与预期原值比较如果 相匹配那么处理器会自动将该位置值更新为新值如果 不匹配处理器不做任何操作多个线程同时执行 CAS 操作只有一个会成功。原理CAS有3个操作数位置内存值V旧的预期值A要修改的更新值B。当且仅当旧的预期值A和内存值V相同时将内存值V修改为B否则什么都不做或重来CAS 是 JDK 提供的非阻塞 原子性操作它通过硬件保证了比较-更新的原子性。它是非阻塞的且自身原子性也就是说这玩意效率更高且通过硬件保证说明这玩意更可靠。CAS 是一条 CPU 的原子指令cmpxchg指令不会造成所谓的数据不一致问题Java 的 Unsafe 提供的 CAS方法如 compareAndSwapXXX底层实现即为 CPU 指令 cmpxchg。执行 cmpxchg 指令的时候会判断当前系统是否为多核系统如果是就给总线加锁只有一个线程会对总线加锁成功加锁成功之后会执行cas操作也就是说 CAS 的原子性实际上是 CPU 实现的 其实在这一点上还是有排他锁的只是比起用 synchronized 这里的排他时间要短的多 所以在多线程情况下性能会比较好Demo:publicstaticvoidmain(String[]args)throwsInterruptedException{AtomicIntegeratomicIntegernewAtomicInteger(5);System.out.println(atomicInteger.compareAndSet(5,2020)\tatomicInteger.get());System.out.println(atomicInteger.compareAndSet(5,1024)\tatomicInteger.get());}2. CAS 底层原理2.1 UnSafecompareAndSet() 方法的源代码上面三个方法都是类似的主要对4个参数做一下说明。var1表示要操作的对象var2表示要操作对象中属性地址的偏移量var4表示需要修改数据的期望的值var5/var6表示需要修改为的新值publicfinalbooleancompareAndSet(intexpectedValue,intnewValue){returnU.compareAndSetInt(this,VALUE,expectedValue,newValue);}其底层是 UnSafe 类的compareAndSet()方法。1 Unsafe是 CAS 的核心类由于 Java 方法无法直接访问底层系统需要通过本地native方法来访问Unsafe 相当于一个后门基于该类可以直接操作特定内存的数据。Unsafe 类存在于 sun.misc 包中其内部方法操作可以像 C 的指针一样直接操作内存因为 Java 中 CAS 操作的执行依赖于 Unsafe 类的方法。注意 Unsafe 类中的所有方法都是 native 修饰的也就是说 Unsafe 类中的方法都直接调用操作系统底层资源执行相应任务。2 变量 valueOffset表示该变量值在内存中的偏移地址因为 Unsafe 就是根据内存偏移地址获取数据的。3 变量 value 用 volatile 修饰保证了多线程之间的内存可见性。2.2 atomicInteger.getAndIncrement(); 如何保证线程安全的我们知道 i 是线程不安全的 那么atomicInteger.getAndIncrement();是如何保证线程安全的呢CAS 是一条 CPU并发原语。它的功能是判断内存某个位置的值是否为预期值如果是则更改为新的值这个过程是原子的。AtomicInteger类主要利用 CAS (compare and swap) volatile 和 native 方法来保证原子操作从而避免 synchronized 的高开销执行效率大为提升。CAS 并发原语体现在 JAVA 语言中就是 sun.misc.Unsafe 类中的各个方法。调用 UnSafe 类中的 CAS 方法JVM 会帮我们实现出 CAS 汇编指令。这是一种完全依赖于硬件的功能通过它实现了原子操作。再次强调由于 CAS 是一种系统原语原语属于操作系统用语范畴是由若干条指令组成的用于完成某个功能的一个过程并且原语的执行必须是连续的在执行过程中不允许被中断也就是说 CAS 是一条 CPU的原子指令不会造成所谓的数据不一致问题。2.3 源码分析newAtomicInteger(10);atomicInteger.getAndIncrement();OpenJDK源码里面查看下Unsafe.java假设线程A 和 线程B 两个线程同时执行 getAndAddInt 操作分别跑在不同CPU上1 AtomicInteger 里面的 value 原始值为3即主内存中 AtomicInteger 的v alue 为3根据 JMM 模型线程A 和 线程B 各自持有一份值为 3 的 value 的副本分别到各自的工作内存。2 线程A 通过 getIntVolatile(var1, var2) 拿到 value 值3这时 线程A 被挂起。3 线程B 也通过 getIntVolatile(var1, var2) 方法获取到 value 值3此时刚好线程B没有被挂起并执行compareAndSwapInt 方法比较内存值也为3成功修改内存值为4线程B打完收工一切OK。4 这时线程A恢复执行 compareAndSwapInt 方法比较发现自己手里的值数字3和主内存的值数字4不一致说明该值已经被其它线程抢先一步修改过了那A线程本次修改失败只能重新读取重新来一遍了。5 线程A重新获取value值因为变量value被volatile修饰所以其它线程对它的修改线程A总是能够看到线程A继续执行 compareAndSwapInt 进行比较替换直到成功。3. 原子引用classUser{StringuserName;intage;}publicclassAtomicReferenceDemo{publicstaticvoidmain(String[]args){Userz3newUser(z3,24);Userli4newUser(li4,26);AtomicReferenceUserarunewAtomicReference();atomicReferenceUser.set(z3);System.out.println(aru.compareAndSet(z3,li4)\taru.get().toString());System.out.println(aru.compareAndSet(z3,li4)\taru.get().toString());}}4. 自旋锁 借鉴CAS思想自旋锁spinlock是指尝试获取锁的线程不会立即阻塞而是采用循环的方式去尝试获取锁当线程发现锁被占用时会不断循环判断锁的状态直到获取。这样的好处是减少线程上下文切换的消耗缺点是循环会消耗CPU.下图是 OpenJDK 源码里面查看下 Unsafe.java5. CAS 缺点5.1 循环时间长 开销很大我们可以看到getAndAddInt方法执行时有个do while如果 CAS 失败会一直进行尝试。如果 CAS 长时间一直不成功可能会给 CPU 带来很大的开销。5.2 ABA 问题CAS会导致“ABA问题”。CAS 算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换那么在这个时间差类会导致数据的变化。比如说一个线程 one 从内存位置V中取出A这时候另一个线程two也从内存中取出A并且线程two进行了一些操作将值变成了B然后线程 two又将V位置的数据变成A这时候线程one进行CAS操作发现内存中仍然是A然后线程one操作成功。尽管线程one的CAS操作成功但是不代表这个过程就是没有问题的。解决ABA问题AtomicStampedReferencepublicclassABADemo{staticAtomicIntegeratomicIntegernewAtomicInteger(100);staticAtomicStampedReferenceatomicStampedReferencenewAtomicStampedReference(100,1);publicstaticvoidmain(String[]args){newThread(()-{atomicInteger.compareAndSet(100,101);atomicInteger.compareAndSet(101,100);},t1).start();newThread(()-{//暂停一会儿线程try{Thread.sleep(500);}catch(InterruptedExceptione){e.printStackTrace();};System.out.println(atomicInteger.compareAndSet(100,2019)\tatomicInteger.get());},t2).start();//暂停一会儿线程,main彻底等待上面的ABA出现演示完成。try{Thread.sleep(2000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println(以下是ABA问题的解决);newThread(()-{intstampatomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName()\t 首次版本号:stamp);//1//暂停一会儿线程,try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()1);System.out.println(Thread.currentThread().getName()\t 2次版本号:atomicStampedReference.getStamp());atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()1);System.out.println(Thread.currentThread().getName()\t 3次版本号:atomicStampedReference.getStamp());},t3).start();newThread(()-{intstampatomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName()\t 首次版本号:stamp);//1//暂停一会儿线程获得初始值100和初始版本号1故意暂停3秒钟让t3线程完成一次ABA操作产生问题try{Thread.sleep(3000);}catch(InterruptedExceptione){e.printStackTrace();}booleanresultatomicStampedReference.compareAndSet(100,2019,stamp,stamp1);System.out.println(Thread.currentThread().getName()\tresult\tatomicStampedReference.getReference());},t4).start();}}