一、CAS简介
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,否则什么都不做。
二、CAS源码分析
我们都知道,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的缺点
从第一、二节可以看出,CAS在java中的实现本质上是使用Unsafe类提供的方法获取对象的内存地址偏移量,进而通过自旋实现的。CAS的优点很明显,那就是区别于悲观策略,乐观策略在高并发下性能表现更好,当然CAS也是有缺点的,主要有类似ABA、自旋时间过长、只能保证一个共享变量原子操作三大问题,下面我们一一分析。
1、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 atomicStampedReference = new AtomicStampedReference(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执行失败了。
2、自旋时间过长
通过第二节分析可以得知,CAS本质上是通过自旋来判断是否更新的,那么问题来了,如果多次旧预期值不等于内存值的情况,那么这个自旋将会自旋下去,而自旋过久将会导致CPU利用率变高。
3、只能保证一个共享变量原子操作
从第二节可以看出,只是单纯对单个共享对象进行CAS操作,保证了其更新获取的原子性,无法对多个共享变量同时进行原子操作。这是CAS的局限所在,但JDK提供同时了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
领取专属 10元无门槛券
私享最新 技术干货