1.CAS
JVM的synchronized重量级锁涉及操作系统内核态下互斥锁的使用,因此其线程阻塞和唤醒都涉及进程在用户态到内核态的频繁切换,导致重量级锁开销大,性能低,而JVM的synchronized轻量级锁使用CAS进行自旋抢锁,CAS是CPU指令级的原子操作,并处于用户态下,所有JVM轻量级锁的开销较小。由于CAS的操作具有原子性,所以在使用CAS方法操作数据时,并不会造成数据不一致性的问题。
2.Unsafe类中的CAS方法
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全的底层操作,如直接访问系统内存资源、自主管理内存资源等,基于C++语言实现,这些方法提升java运行效率,增强java语言底层资源操作能力方面起到了很大的作用。
3.java获取CAS操作
(1)获取Unsafe实例。由于类是一个find修饰的类不允许继承的最终类,其构造函数是private类型的,因此不能实例化,可以通过反射的方式自定义地获取Unsafe实例的辅助方法。
(2)调用Unsafe提供的CAS方法,这些方法主要封装了底层CPU的CAS原子操作。Unsafe提供的CAS方法包含4个操作数——字段所在的对象、字段内存位置、预期原值及新值。在执行Unsafe的CAS方法时,这些方法首先将内存位置的值与预期值(旧的值)比较,如果相匹配,那么CPU会自动将该内存位置的值更新为新值,并返回true;如果不匹配,CPU不做任何操作,并返回false。Unsafe的CAS操作会将第一个参数(对象的指针、地址)与第二个参数(字段偏移量)组合在一起,计算出最终的内存操作地址。
(3)调用Unsafe提供的字段偏移量方法,这些方法用于获取对象中的字段(属性)偏移量,此偏移量值需要作为参数提供给CAS操作
4.使用CAS进行无锁编程
CAS是一种无锁算法,该算法关键依赖两个值-期望值(旧值)和新值,底层CPU利用原子操作判断内存原值与期望值是否相等,如果相等就给内存地址赋新值否则不做任何操作。使用CAS进行无锁编程步骤大致如下:
(1).获得字段的期望值
(2).计算出需要替换的新值
(3).通过CAS将新值放在字段的内存地址上,如果CAS失败就将重复第(1)和第(2)步骤,一直到CAS成功,这种重复称为CAS自旋。
5.原子类
Atomic操作翻译成中文是指一个不可中断的操作,即使在多个线程一起执行Atomic类型操作的时候,一个操作一旦开始,就不会被其他线程中断,所谓Atomic类,值的具有原子操作特征的类。
6.原子类分类
(1).基本原子类通过原子的方式更新java基础类型变量的值,基本原子类主要包括一下三个:整型原子类、长整型原子类、布尔型原子类,线程是安全的。其原理为:通过CAS自旋+volatile的方案实现,既保障了变量操作的线程安全性,有避免了synchronized重量级锁的高开销,使得java程序的执行效率大为提示。
(2).数组原子类通过原子方式更数组中某个元素的值,数组原子类主要包括一下三个:整型数组原子类、长整型数组原子类、引用类型数组原子类,引用类型的原子操作,只能保证引用的原子性,如果引用的为对象时,只能保证对象的原子性,不能保证对象属性的原子性。如果需要保证对象属性的原子性,就需要用到属性更新原子类。
(3).引用原子类,主要有以下几种:引用类型原子类、带有更新标记位的原子引用类型、带有更新版本号的原子引用类型
(4).字段更新原子类,主要包括一下三个:原子更新整型字段的更新器、原子更新长整型字段的更新、原子更新引用类型的字段
7.ABA问题
一个线程A从内存位置M中取出V1,另一个线程B也取出V1,现在假设线程B进行了一些操作之后将M位置的数据V1变成V2,然后又在一些操作V2变成了V1。之后线程A进程CAS操作,但是线程A发现M位置数据仍然是V1,然后线程A操作成功。尽管线程A的CAS操作成功,但是不代表这个过程没有问题,线程A操作的数据V1可能不是之前的V1,而是被线程B替换过的V1,这就是ABA问题。
8.ABA的解决方案
使用版本号(version)方式来解决,乐观锁每次在执行数据操作是都会带上一个版本号,版本号和数据的版本号一致就可以执行修改操作并对版本号执行加1操作,否则执行失败,因为每次操作的版本号都会随之增加,所以不会出现ABA的问题,因为版本号只会增加,不会减少。
9.如何提高CAS操作的性能
使用LongAdder类,以空间换取时间的方式提示高并发场景,其核心思想是:热点分离,与ConcurrentHashMap的设计思想类似:将value值分离成一个数组,当多线程访问时,通过Hash算法,将线程映射到数组的一个元素进行操作;而获取最终的value结果时,则将数组的元素进行求和。最终将单个value值演变成一系列的数组元素,从而减少了内部竞争的粒度。
10.LongAdder核心原理
AtomicLong使用内部变量value保存着实际的long值,所有的操作都是针对该value变量进行的。也就是说,在高并发环境下,value变量其实是一个热点,也就是N个线程竞争一个热点。重试线程越多,就意味着CAS的失败概率更高,从而进入恶性CAS空自旋状态。LongAdder的基本思路是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽(元素)中,各个线程只对自己槽中的那个值进行CAS操作。这样热点就被分散了,冲突的概率就小很多。使用LongAdder,即使线程数再多也不必担心,各个线程会分配到多个元素上去更新,增加元素个数,就可以降低value的“热度”,AtomicLong中的恶性CAS空自旋就解决了。如果要获得完整的LongAdder存储的值,只要将各个槽中的变量值累加,返回最终累加之后的值即可。LongAdder的实现思路与ConcurrentHashMap中分段锁的基本原理非常相似,本质上都是不同的线程在不同的单元上进行操作,这样减少了线程竞争,提高了并发效率。LongAdder的设计体现了空间换时间的思想,不过在实际高并发场景下,数组元素所消耗的空间可以忽略不计。
11CAS操作的弊端与规避措施
(1).ABA问题
解决思路:引入版本号,变量前面加上版本号,每次变量更新是将版本号加1,那么操作序列A==>B==>A就会变成A1==>B2==>A3,如果将A1当作A3的预期数据,就会操作失败。JDK提供了两个类AtomicStampedReference和AtomicMarkableReference来解决ABA问题。比较常用的是AtomicStampedReference类,该类的compareAndSet()方法的作用是首先检查当前引用是否等于预期引用,以及当前印戳是否等于预期印戳,如果全部相等,就以原子方式将引用和印戳的值一同设置为新的值。
(2).只能保证一个共享变量之间的原子性操作(当一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,CAS就无法保证原子性)
解决思路:把多个共享变量合并成一个共享变量来操作,JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个AtomicReference实例后再进行CAS操作。比如有两个共享变量i=1、j=2,可以将二者合并成一个对象,然后用CAS来操作该合并对象的AtomicReference引用。
(3).开销问题(自旋CAS如果长时间不成功(不成功就一直循环执行,直到成功)),就会给CPU带来非常大的执行开销
解决思路:以空间换时间 (1).分散操作热点 使用LongAdder 替换基础原子类,是其单个CAS热点分散到一个cells数组中。
(2)使用队列削峰,将发生CAS争用的线程加入一个队列中排列,降低CAS争用的激烈程度,JUC中非常重要的基础类AQS(抽象队列同步器)就是这么做的。