CAS即 Compare And Swap
对比交换,区别于悲观锁,借助CAS可以实现区别于synchronized独占锁的一种乐观锁,被广泛应用在各大编程语言之中。Java JUC底层大量使用了CAS,可以说 java.util.concurrent
完全是建立在CAS之上的。但是CAS也有相应的缺点,诸如 ABA
、 cpu使用率高
等问题无法避免。
CAS总共有3个操作数,当前内存值V,旧的预期值A,要修改的新值N。当且仅当A和V相同时,将V修改为N,否则什么都不做。
我们都知道,java提供了一些列并发安全的原子操作类,如 AtomicInteger
、 AtomicLong
.下面我们拿 AtomicInteger
为例分析其源码实现。
// 1、获取UnSafe实例对象,用于对内存进行相关操作 private static final Unsafe unsafe = Unsafe.getUnsafe(); // 2、内存偏移量 private static final long valueOffset; static { try { // 3、初始化地址偏移量 valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } // 4、具体值,使用volatile保证可见性 private volatile int value; 复制代码
从上面代码中我们可以看出, AtomicInteger
中依赖于一个叫 Unsafe
的实例对象,我们都知道,java语言屏蔽了像C++那样直接操作内存的操作,程序员不需手动管理内存,但话说回来,java还是开放了一个叫 Unsafe
的类直接对内存进行操作,由其名字可以看出,使用 Unsafe
中的操作是不安全的,要小心谨慎。
valueOffset
是对象的内存偏移地址,通过 Unsafe
对象进行初始化,有一点需要注意的是,对于给定的某个字段都会有相同的偏移量,同一类中的两个不同字段永远不会有相同的偏移量。也就是说,只要对象不死,这个偏移量就永远不会变,可以想象,CAS所依赖的第一个参数(内存地址值)正是通过这个地址偏移量进行获取的。
value
属于共享资源,借助 volatile
保证内存可见性,关于 volatile
的简单分析,可以参考
Java 反汇编、反编译、volitale解读
// 1、获取并增加delta public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } // 2、加一 public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } 复制代码
上面两个方法依赖下面 Unsafe
类中的 getAndAddInt
操作,借助 openjdk
提供的 Unsafe
源码,我们看下其实现:
public final int getAndAddInt(Object o, long offset, int delta) { int v; // 1、不断的循环比较,直到CAS操作成功返回 do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; } 复制代码
从上面可以看出,本质上CAS使用了自旋锁进行自旋,直到CAS操作成功,如果很长一段时间都没有操作成功,那么将一直自旋下去。
从第一、二节可以看出,CAS在java中的实现本质上是使用 Unsafe
类提供的方法获取对象的内存地址偏移量,进而通过自旋实现的。CAS的优点很明显,那就是区别于悲观策略,乐观策略在高并发下性能表现更好,当然CAS也是有缺点的,主要有类似 ABA
、 自旋时间过长
、 只能保证一个共享变量原子操作
三大问题,下面我们一一分析。
什么是ABA呢?简单的说,就是有两个线程,线程A和线程B,对于同一个变量X=0,A准备将X置为10,按照CAS的步骤,首先会从内存读取值旧的预期值0,然后比较,最后置为10,但就在A读取完X=0后,还没来得及比较和赋值,此时线程B完成了 X=0 -> X=10 -> X=0
这3个操作,随后A继续执行比较,发现此时内存的值依旧是0,最后CAS执行成功。虽然过程和结果没有问题,但是A比较时的0已经不是最初那个0了,有种被偷梁换柱的感觉。
下面代码举例演示 ABA
问题,线程1模拟将变量从100->110->100,线程2执行100->120,最后看下输出:
import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicStampedReference; /** * * @Author jiawei huang * @Since 2020年1月17日 * @Version 1.0 */ public class ABATest { // 初始值为100 private static AtomicInteger atomicInteger = new AtomicInteger(100); public static void main(String[] args) throws InterruptedException { // AtomicInteger实现 100->110->100 Thread t1 = new Thread(new Runnable() { @Override public void run() { atomicInteger.compareAndSet(100, 110); atomicInteger.compareAndSet(110, 100); } }); // 实现 100->120 Thread t2 = new Thread(new Runnable() { @Override public void run() { try { // 这里模拟线程1执行完毕,偷梁换柱成功 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } // 下面依旧返回true System.out.println("AtomicInteger:" + atomicInteger.compareAndSet(100, 120)); } }); t1.start(); t2.start(); } } 复制代码
输出结果为:
AtomicInteger:true 复制代码
可见线程2中的CAS也执行成功了,那么如何解决这个问题呢?解决方案是通过版本号,Java提供了 AtomicStampedReference
来解决。 AtomicStampedReference
通过包装 [E,Integer]
的元组来对对象标记版本戳 stamp
,从而避免 ABA
问题。
/* * Copyright (C) 2011-2019 DL * * All right reserved. * * This software is the confidential and proprietary information of DL of China. * ("Confidential Information"). You shall not disclose such Confidential * Information and shall use it only in accordance with the argeements * reached into with DL himself. * */ package com.algorithm.leetcode.linkedlist; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicStampedReference; /** * * @Author jiawei huang * @Since 2020年1月17日 * @Version 1.0 */ public class ABATest { // 初始值100,版本号1 private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100, 1); public static void main(String[] args) throws InterruptedException { // AtomicStampedReference实现 Thread tsf1 = new Thread(new Runnable() { @Override public void run() { try { // 让 tsf2先获取stamp,导致预期时间戳不一致 TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } // 预期引用:100,更新后的引用:110,预期标识getStamp() 更新后的标识getStamp() + 1 atomicStampedReference.compareAndSet(100, 110, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); atomicStampedReference.compareAndSet(110, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); } }); Thread tsf2 = new Thread(new Runnable() { @Override public void run() { int stamp = atomicStampedReference.getStamp(); try { TimeUnit.SECONDS.sleep(2); // 线程tsf1执行完 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println( "AtomicStampedReference:" + atomicStampedReference.compareAndSet(100, 120, stamp, stamp + 1)); } }); tsf1.start(); tsf2.start(); } } 复制代码
输出结果:
AtomicStampedReference:false 复制代码
可以看出线程1执行失败了。
通过第二节分析可以得知,CAS本质上是通过自旋来判断是否更新的,那么问题来了,如果多次旧预期值不等于内存值的情况,那么这个自旋将会自旋下去,而自旋过久将会导致CPU利用率变高。
从第二节可以看出,只是单纯对单个共享对象进行CAS操作,保证了其更新获取的原子性,无法对多个共享变量同时进行原子操作。这是CAS的局限所在,但JDK提供同时了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。