CAS
CAS(Compare And Swap,比较并交换)自旋抢锁。
微信交流群:Java技术沟通群⑤(点击加入)
原理
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。
CAS 操作用得比较多的是 sun.misc 包的 Unsafe 类,UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
Java 并发包大量使用 Unsafe 类的 CAS 操作。AtomicXXX原子类(本质是自旋锁 + CAS)
使用CAS进行无锁编程
CAS是一种无锁算法,该算法关键依赖两个值——期望值(旧值) 和新值,底层CPU利用原子操作判断内存原值与期望值是否相等,如果 相等就给内存地址赋新值,否则不做任何操作。
使用CAS进行无锁编程的步骤大致如下:
(1)获得字段的期望值(oldValue)。
(2)计算出需要替换的新值(newValue)。
(3)通过CAS将新值(newValue)放在字段的内存地址上,如果 CAS失败就重复第(1)步到第(2)步,一直到CAS成功,这种重复俗 称CAS自旋。
接下来执行线程B的CAS(100,300)操作,此时内存地址的值为200, 不等于CAS的期望值100,线程B操作失败。线程B只能自旋,开始新的 循环,这一轮循环首先获取到内存地址的值200,然后进行 CAS(200,300)操作,这一次内存地址的值与CAS的预期值(oldValue) 相等,线程B操作成功。
当CAS将内存地址的值与预期值进行比较时,如果相等,就证明内存地址的值没有被修改,可以替换成新值,然后继续往下运行;如果不相等,就说明内存地址的值已经被修改,放弃替换操作,然后重新自 旋。当并发修改的线程少,冲突出现的机会少时,自旋的次数也会很 少,CAS的性能会很高;当并发修改的线程多,冲突出现的机会多时, 自旋的次数也会很多,CAS的性能会大大降低。所以,提升CAS无锁编 程效率的关键在于减少冲突的机会。
CAS操作的弊端和规避措施
CAS操作的弊端主要有以下三点:
1.ABA问题
存在 ABA 问题,即原来内存地址的值是 A,然后被改为了 B,再被改为 A 值,此时 CAS 操作时认为该值未被改动过,ABA 问题可以引入版本号来解决,每次改动都让版本号 +1。Java 中处理 ABA 的一个方案是 AtomicStampedReference 类,它是使用一个 int 类型的字段作为版本号,每次修改之前都先获取版本号和当前线程持有的版本号比对,如果一致才进行修改操作,并把版本号 +1。
2.只能保证一个共享变量之间的原子性操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,CAS就无法保证操作的原子性。
一个比较简单的规避方法为:把多个共享变量合并成一个共享变量来操作。
JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个AtomicReference实例后再进行CAS操作。比如有两 个共享变量i=1、j=2,可以将二者合并成一个对象,然后用CAS来操作该合并对象的AtomicReference引用。
3.开销问题
自旋CAS如果长时间不成功(不成功就一直循环执行,直到成功),就会给CPU带来非常大的执行开销。
解决CAS恶性空自旋的有效方式之一是以空间换时间,较为常见的方案为:
(1)分散操作热点,使用LongAdder替代基础原子类 AtomicLong,LongAdder将单个CAS热点(value值)分散到一个cells数 组中。
(2)使用队列削峰,将发生CAS争用的线程加入一个队列中排队,降低CAS争用的激烈程度。JUC中非常重要的基础类AQS(抽象队列同步器)就是这么做的。
CAS操作在JDK中的应用
CAS在java.util.concurrent.atomic包中的原子类、Java AQS以及显式锁、CurrentHashMap等重要并发容器类的实现都有非常广泛的应用。
在java.util.concurrent.atomic包的原子类(如AtomicXXX)中都使用 了CAS来保障对数字成员进行操作的原子性。
java.util.concurrent的大多数类(包括显式锁、并发容器)都是基于 AQS和AtomicXXX来实现的,其中AQS通过CAS保障它内部双向队列头部、尾部操作的原子性。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/279052.html