CAS即Compare And Swap
对比交换,区别于悲观锁,借助CAS能够实现区别于synchronized独占锁的一种乐观锁,被普遍应用在各大编程语言之中。Java JUC底层大量使用了CAS,能够说java.util.concurrent
彻底是创建在CAS之上的。可是CAS也有相应的缺点,诸如ABA
、cpu使用率高
等问题没法避免。java
CAS总共有3个操做数,当前内存值V,旧的预期值A,要修改的新值N。当且仅当A和V相同时,将V修改成N,不然什么都不作。程序员
咱们都知道,java提供了一些列并发安全的原子操做类,如AtomicInteger
、AtomicLong
.下面咱们拿AtomicInteger
为例分析其源码实现。编程
// 一、获取UnSafe实例对象,用于对内存进行相关操做
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 二、内存偏移量
private static final long valueOffset;
static {
try {
// 三、初始化地址偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 四、具体值,使用volatile保证可见性
private volatile int value;
复制代码
从上面代码中咱们能够看出,AtomicInteger
中依赖于一个叫Unsafe
的实例对象,咱们都知道,java语言屏蔽了像C++那样直接操做内存的操做,程序员不需手动管理内存,但话说回来,java仍是开放了一个叫Unsafe
的类直接对内存进行操做,由其名字能够看出,使用Unsafe
中的操做是不安全的,要当心谨慎。安全
valueOffset
是对象的内存偏移地址,经过Unsafe
对象进行初始化,有一点须要注意的是,对于给定的某个字段都会有相同的偏移量,同一类中的两个不一样字段永远不会有相同的偏移量。也就是说,只要对象不死,这个偏移量就永远不会变,能够想象,CAS所依赖的第一个参数(内存地址值)正是经过这个地址偏移量进行获取的。bash
value
属于共享资源,借助volatile
保证内存可见性,关于volatile
的简单分析,能够参考并发
// 一、获取并增长delta
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
// 二、加一
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
复制代码
上面两个方法依赖下面Unsafe
类中的getAndAddInt
操做,借助openjdk
提供的Unsafe
源码,咱们看下其实现:ide
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
// 一、不断的循环比较,直到CAS操做成功返回
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
复制代码
从上面能够看出,本质上CAS使用了自旋锁进行自旋,直到CAS操做成功,若是很长一段时间都没有操做成功,那么将一直自旋下去。高并发
从第1、二节能够看出,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操做。