1.概述
CAS(Compare And Swap)比较并交换,它是一种乐观锁的实现方式。其作用是让CPU比较内存中某个值是否和预期的值相同,如果相同则将这个值更新为新值,不相同则不做更新,也就是CAS是原子性的操作(读和写两者同时具有原子性),由于其底层是基于C/C++实现,因而具有很高的性能。本文首先会介绍CAS的基本思想以及相关基础知识(乐观锁和悲观锁),其次讲解一个小案例来进一步阐述CAS思想,最后分析CAS源码,让读者对CAS有更进一步的了解。
2.基础原理解释
2.1 乐观锁(非互斥同步锁)
2.1.1 定义
乐观锁总是假设最好的情况,也就是每次获取数据时,总是乐观地以为别人没有修改过该数据,所以不会上锁。但在更新数据时,会判断在此期间有没有别人更新过该数据。因为在获取数据时没有加锁,因而省去了一部分开销,加大了系统吞吐量。乐观锁主要适用于读多写少的场景,能够大幅度提升性能。
2.1.2 优势
在读多写少的环境下,能够有效提升性能。
2.1.3 劣势
1.ABA问题:一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过,进而进行更改等操作引发问题;
2.自旋性能消耗:自旋CAS(不成功,就一直循环执行,直到成功)如果长时间不成功,会给CPU带来极大的执行开销。
2.1.4 使用场景
1.CAS实现:Java中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种CAS实现方式;
2.版本号控制:一般做法实在数据表中加一个version字段,表示数据被修改的次数,数据每被修改一次,version就进行加一操作;当线程读取该值时,会同时读取该值的version版本号,在更新时会再一次对比刚才获取的版本号,一致时才进行修改,否则一致自旋获取,直到更新成功。
2.2 悲观锁(互斥同步锁)
2.2.1 定义
悲观锁总是假设最坏的情况,每次去拿锁是就认为别人已经对数据进行了修改,所以每次去获取数据时都会加锁,一旦获取了锁,其它想获取数据的线程就要挂起等待,直到锁被释放。
2.2.2 优势
相对来说更能保证数据安全。
2.2.3 劣势
1.在多线程竞争条件下,频繁加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题;
2.一个线程尺有锁会导致其它需要此锁的线程挂起;
3.如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
2.2.4 使用场景
1.JAVA中synchronized关键字的实现;
2.传统关系型数据库的锁机制,比如行锁、表锁、读锁、写锁,都是在操作之前先上锁;
2.3 CAS原理
2.3.1 一个小案例
首先来看实现一个小案例:有10个线程,对同一个变量做修改操作,每次操作+1,执行100次,保证最后的结果正确。大部分人会写出如下代码:
public class CASTest {
private static Integer threadSize = 100;
private static Integer count = 0;
public static synchronized Integer request() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(5);
return count++;
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
long startTime = System.currentTimeMillis();
for (int i = 0; i < threadSize; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println("耗费时间:" + (endTime - startTime) + ",count = :" + count);
}
}
这中代码中使用了JAVA关键字synchroized,synhcorized是典型的悲观锁实现。基于上述代码,我进行了耗费时间的统计(win10环境,16G运行内存,4核CPU),执行次数为5次,执行时间如下:
序号 | 耗时(ms) |
---|---|
1 | 5849 |
2 | 5804 |
3 | 5828 |
4 | 5790 |
5 | 5800 |
平均时间 | 5814.2 |
或许一部分人还会写出这种代码:
public class CASTest1 {
private static Integer threadSize = 100;
private static Integer count = 0;
public static Integer request() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(5);
return count++;
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
long startTime = System.currentTimeMillis();
Lock lock = new ReentrantLock();
for (int i = 0; i < threadSize; i++) {
new Thread(() -> {
lock.lock();
try {
for (int j = 0; j < 10; j++) {
request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println("耗费时间:" + (endTime - startTime) + ",count = :" + count);
}
}
上述写法,在同等条件下,再次统计5次的执行时间,具体如下表所示:
序号 | 耗时(ms) |
---|---|
1 | 5802 |
2 | 5821 |
3 | 5804 |
4 | 5810 |
5 | 5841 |
平均时间 | 5815.6 |
2.3.2 案例优化
鉴于上述写法的性能消耗,于是寻求一种优化方案来提升性能;借鉴CAS中的思想,可对上述代码做出如下优化:
public class CAS {
private static Integer threadSize = 100;
private static Integer count = 0;
public static Integer request() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(5);
Integer expected = 0;
while (!compareAndSwap(expected = getCount(), getCount() + 1)) {
}
return count;
}
public static Integer getCount() {
return count;
}
public static synchronized boolean compareAndSwap(Integer expectedValue, Integer newValue) {
if (expectedValue.equals(getCount())) {
count = newValue;
return true;
}
return false;
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
long startTime = System.currentTimeMillis();
for (int i = 0; i < threadSize; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
request();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
long endTime = System.currentTimeMillis();
System.out.println("耗费时间:" + (endTime - startTime) + ",count = :" + count);
}
}
针对上述这种写法,在同等条件下,进行时间性能测试,测试结果如下表所示:
序号 | 耗时(ms) |
---|---|
1 | 106 |
2 | 102 |
3 | 106 |
4 | 109 |
5 | 105 |
平均时间 | 105.6 |
由上述平均耗费时间对比可知,优化后的执行时间差不多是未优化前的1/50,性能提升了近50倍,这也太强了吧!!!
2.3.3 CAS源码分析
java提供了对CAS的支持,主要在sun.misc.unsafe类中,具体如下:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
上述属性的解释如下:
参数var1:表示要操作的对象;
参数var2:表示要操作对象中属性的偏移量(对象存储在堆中,会有一个地址,但对象内的属性相对于对象地址会有一定偏移,因此还需要知道该对象指定属性的地址,方能进行修改);
参数var4:表示需要修改数据的期望的值;
参数var5:表示需要修改的新值。
2.3.4 CAS实现原理
CAS通过调用JNI的代码实现(JNI:java Native Interface),允许java调用其它语言。上述CompareAndSwapObject/Int/Long的几个方法就是借助C语言来调用cpu底层指令实现的。
2.4 ABA问题
2.4.1 ABA问题定义
ABA问题指的是CAS在执行操作值的时候会检查该值有没有发生变化,如果没有发生变化则进行更新操作,但是如果一个值原来是A,在CAS方法执行之前被其它线程改成了B,然后又被改回了A,那么CAS方法执行检查的时候会发现它的值没有变化,但是实际却发生了改变,这就是CAS的ABA问题。
2.4.2 ABA问题简单代码演示
@Slf4j
public class ABATest1 {
private static AtomicInteger a = new AtomicInteger(5);
public static void main(String[] args) {
new Thread(() -> {
log.info("线程:{}开始休眠", Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
log.error("error:{}", e);
}
boolean b = a.compareAndSet(5, 100);
log.info("b={}", b);
log.info("a={}", a.get());
}, "Thread-main").start();
new Thread(() -> {
boolean b = a.compareAndSet(5, 10);
log.info("b={}", b);
boolean b1 = a.compareAndSet(10, 5);
log.info("b1={}", b1);
}, "Thread-slave").start();
}
}
运行结果如下图所示:
2.4.3 ABA问题解决方案
java中主要通过AtomicStampedReference类来对ABA问题进行解决,它的实现原理就是给值加了一个修改版本号,每次值变化,都会修改它的版本号,CAS操作时都去对比此版本号。AtomicStampedReference主要包含一个对象引用以及一个可以自动更新整数版本的pair对象来解决ABA问题。
下面看一下该类的源码:
public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference; //数据引用
final int stamp; //版本号
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
//of方法用来生成新Pair对象,需要传入对象和版本号
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
public V getReference() {
return pair.reference;
}
public int getStamp() {
return pair.stamp;
}
public V get(int[] stampHolder) {
Pair<V> pair = this.pair;
stampHolder[0] = pair.stamp;
return pair.reference;
}
public boolean weakCompareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
return compareAndSet(expectedReference, newReference,
expectedStamp, newStamp);
}
public boolean compareAndSet(V expectedReference, //期望引用对象
V newReference, //新引用对象
int expectedStamp, //期望版本
int newStamp) { //新版本
Pair<V> current = pair;
return
expectedReference == current.reference && //期望引用与当前引用一致
expectedStamp == current.stamp && //期望版本与当前版本一致
//判断新引用对象与当前引用对象是否一致(包括版本),若不一致,重新创建Pair对象并设置到AtomicStampedReference中
((newReference == current.reference && newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
public void set(V newReference, int newStamp) {
Pair<V> current = pair;
if (newReference != current.reference || newStamp != current.stamp)
this.pair = Pair.of(newReference, newStamp);
}
public boolean attemptStamp(V expectedReference, int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
(newStamp == current.stamp ||
casPair(current, Pair.of(expectedReference, newStamp)));
}
private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
private static final long pairOffset =
objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
//通过调用UNSAFE的compareAndSwapObject方法来重新设置pair对象
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
static long objectFieldOffset(sun.misc.Unsafe UNSAFE,
String field, Class<?> klazz) {
try {
return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));
} catch (NoSuchFieldException e) {
// Convert Exception to corresponding Error
NoSuchFieldError error = new NoSuchFieldError(field);
error.initCause(e);
throw error;
}
}
}
2.4.5 AtomicStampedReference演示
@Slf4j
public class ABATest2 {
private static AtomicStampedReference<Integer> a = new AtomicStampedReference(5, 1);
public static void main(String[] args) {
new Thread(() -> {
Integer stamp = a.getStamp();
log.info("线程:{}开始休眠", Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
log.error("error:{}", e);
}
Integer expectedReference = 5;
Integer newReference = 100;
Integer newStamp = stamp + 1;
boolean b = a.compareAndSet(expectedReference, newReference, stamp, newStamp);
log.info("b={}", b);
log.info("a={}", a.getReference());
}, "Thread-main").start();
new Thread(() -> {
boolean b = a.compareAndSet(a.getReference(), a.getReference() + 5, a.getStamp(), a.getStamp() + 1);
log.info("b={}", b);
boolean b1 = a.compareAndSet(a.getReference(), a.getReference() - 5, a.getStamp(), a.getStamp() + 1);
log.info("b1={}", b1);
}, "Thread-slave").start();
}
}
运行结果为:
由上述结果可知,辅助线程虽然触发了ABA,但是主线程已经发现并作了对比,由于对比版本时发现不一致,故而结果为false。
3.小结
1.CAS的使用场景:针对于数据并发场景较高,数据安全性不是特别高的场景。若对于数据安全性能要求较高,比如银行这种系统,建议使用关键字synchronized来进行数据同步操作;
2.AtomicStampedReference通过增加stamp(版本号)这个属性,保证了不同线程变更数据时,版本号一定会发生变化来保证数据唯一性;
3.CAS虽然较为高效,如果并发量较大,CAS长时间操作不成功,会导致其一直自旋,对CPU的消耗较大。
4.参考文献
1.https://www.bilibili.com/video/BV1kE411u7bj?p=3&spm_id_from=pageDriver
2.https://juejin.cn/post/6993910426480164901
3.https://juejin.cn/post/6921974505652879367