什么是CAS?
CAS(Compare and Swap)比较并交换。里面涉及到unSave类和自旋锁。
CAS是一条CPU并发原语,它的功能是判断内存中某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
public class CASTest {
public static void main(String[] args) {
//新建一个原子整型实例,初始值设置为6,不设置则默认为0
AtomicInteger integer = new AtomicInteger(6);
System.out.println(integer.compareAndSet(6, 8)+"\t 当前值="+integer.get());
System.out.println(integer.compareAndSet(6, 66)+"\t 当前值="+integer.get());
}
}
--- - - - - - - - - - - --- - - - - -
true 当前值=8
false 当前值=8
先比较后交换 核心方法就是上面代码中的 integer.compareAndSet(6, 8);第一个参数代表期望值,第二个参数表示最后更新成的数值。上面代码在第一次打印的时候,期望值是6,更新值是8,由于该原子整型在声明时设置的初始值就是6,所以先比较,发现初始值和期望值是相等的,然后交换该值为8;但在第二次打印时,此时内存中的数值已经被更新成8,这时再比较发现8和期望值6不相等,所以无法更新,最后的值还是8。
上图中线程B更新失败,此时线程B只能重新从主内存中拷贝最新的变量副本,重新赋值,重新写回主内存,如果依然比其他线程慢一拍,导致比较不相等,则循环以上操作,直到比较成功相等为止。
CAS底层原理
自旋锁+UnSafe类
这里引出上篇文章中 ( Volatile详解 ) 提到的问题:为什么“加个Atomic”就能保证原子性呢?看AtomicInteger源码先:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
//CAS之所以能保证原子性,靠的就是这里的UnSafe类
//UnSafe类路径:C:\Program Files\Java\jdk1.8.0_162\jre\lib\rt\sun\misc下
private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
//这个VALUE是内存地址偏移量,这样Unsafe类就可以不用知道该地址的变量值
//只需要知道地址的偏移量就可以通过C的指针来操作数据
private static final long VALUE;
static {
try {
VALUE = U.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
}
//这里的value便是AtomicInteger中的变量,用volatile修饰,保证其可见性
private volatile int value;
...
...
...
}
Unsafe类是CAS的核心类,类中所有的方法都是native修饰的,所以Unsafe类中的方法都直接调用操作系统底层资源执行相应的任务。由于Java方法无法直接访问底层系统,需要通过本地(native方法)来访问,基于Unsafe类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C语言的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的方法时,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作。由于CAS是系统原语,原语又属于操作系统用语范畴,由若干条指令组成,用于完成某个功能的一个过程。并且原子的执行必须是连续的,在执行过程中不允许被中断,所以CAS指令不会造成数据不一致问题。
接着尝试深挖一下Unsafe类的源码,如下:
AtomicInteger integer = new AtomicInteger(6);
integer.getAndIncrement();
---点进 getAndIncrement(); 方法---
public final int getAndIncrement() {
//U表示Unsafe类的实例,this表示AtomicInteger类,VALUE表示变量地址偏移量,1就是最后要加的1.
return U.getAndAddInt(this, VALUE, 1);
}
---点进 getAndAddInt 方法---
public final int getAndAddInt(java.lang.Object o,long l,int i) {/* compiled code */}
---getAndAddInt 底层源码是---
//unsafe.getAndAddInt
public final int getAndAddInt(Object var1,long var2,int var4){
int var5;
do{
//这里var1是当前对象AtomicInteger ,var2是地址偏移量VALUE
//通过这俩参数获取该地址的真实值var5
var5 = this.getIntVolatile(var1,var2);
} while(!this.compareAndSwapInt(var1,var2,var5,var5 + var4));
return var5;
}
可以看到,源码最后用了一个do...while循环,大致步骤为:
- 先通过var1和var2这两个参数获取主内存中的真实值var5;
- 用该对象var1的当前值与var5比较,若相等,则加var4(即1)并返回;
- 若不相等,则继续循环1、2步骤,直到相等。
以上是我个人理解,下面请看详细步骤:
假设有线程A和B,同时执行getAndAddInt方法
- AtomicInteger初始值为6,即主内存中的值为6,根据JMM模型,线程A和B的工作内存中都有主内存变量值6的拷贝副本
- 线程A通过 getIntVolatile 方法得到值6,假设此时A突然被挂起(线程的执行和挂起由CPU调度控制)
- 线程B也通过 getIntVolatile 方法得到值6,幸运的是B没有被挂起,执行 compareAndSwapInt 方法比较主内存值也是6,比较成功并 6 + 1 = 7,最后将7写回主内存,此时主内存值变为7,线程B打完收工
- 此时线程A恢复执行,通过 compareAndSwapInt 比较发现内存中的值是 7 不是 6 ,说明自己来晚了一步,主内存的值被其他人动过了,比较失败只能重新获取最新值再进行比较
- 线程A重新获取值 7 ,由于变量被volatile修饰,所以任何线程对变量的修改,其他线程都能看得到,线程A继续执行compareAndSwapInt 方法进行比较替换,直到成功
compareAndSwapInt 方法内部代码:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env,jobject unsafe,jobject obj,jlong offset,jint e,jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt");
opp p = JNIHandles::resolve(obj);
//想办法拿到变量的内存地址
jint* addr = (jint*)index_opp_from_field_offset_long(p,offset);
//比较替换,x是即将更新的值,e是原内存的值
return (jint)(Atomic::cmpxchg(x,addr,e)) == e;
UNSAFE_END
为什么要用CAS而不是用synchronized?
因为synchronized会加锁,同一时间段只允许一个线程访问,一致性得到了保障,但是并发性下降;
而CAS没有用锁,而是通过循环的方式进行CAS比较,直到比较成功为止,既保证了一致性,又不影响并发性。
CAS的缺点
- 如果CAS长时间比较失败,CPU开销会比较大
- 对于多个共享变量的操作,CAS就无法保证其原子性,此时可以用锁来代替
- 引发ABA问题
什么是ABA问题?
CAS算法实现的一个重要前提是需要取出内存中某一时刻的数据并在当下时刻进行比较并替换,那么在这个时间差内有可能数据已经变化了多次。倘若在这个时间差内,数据改变了多次并最终又变回最初的数值,而其他并行处理的线程并无察觉数据的改变,依然可以通过CAS法则进行比较替换,但这并不代表没有问题。——此为ABA问题的来源
假如有两个线程1和2同时操作一个变量value,value初始值为A,线程1处理变量的时间为5s,线程2处理变量的时间为2s,这种情况下,肯定线程2会强先一步完成value的操作,假如线程2在第2秒将value改位了B并写回主内存,又在第四秒将B又改回了A并写入了主内存,接着在第5秒,线程1执行完毕要将它的最新值写回主内存,在经过CAS比较时,发现主内存是A,跟自己想要的一样,但此时的A已经不是原来那个A,是已经被线程2操作了两次改成的A。虽然线程2也能顺利完成比较并替换,但这是有问题的,也就是ABA问题。
如何避免ABA问题?
在此之前,先要了解什么是“原子引用”。
举个栗子 ↓?↓
//首先定义个User类
class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class ABADemo {
public static void main(String[] args) {
User z3 = new User("张三",30);
User l4 = new User("李四",40);
User w5 = new User("王五",50);
//此为原子引用类,此时引用的是User,初始值设置为:张三
AtomicReference<User> atomicReference = new AtomicReference<>(z3);
System.out.println(atomicReference.compareAndSet(z3, l4)+"\t 当前人="+atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3, w5)+"\t 当前人="+atomicReference.get().toString());
}
}
---打印结果-— - - - -- - - - - - - - - - -
true 当前人=User{name='李四', age=40}
false 当前人=User{name='李四', age=40}
话不多说,原理跟上面的AtomicInteger一模一样。
吃完这个栗子↑?↑,我们再来YY如何避免ABA问题——给变量附加一个版本号(类似于时间戳)
这里再引入一个概念:AtomicStampedReference——有邮戳的原子引用
/**
*这里举一个最简单的栗子
*/
public class ABATest {
public static void main(String[] args) {
//初始化一个有邮戳的原子引用,并设置引用的初始值和初始版本号为A和0
AtomicStampedReference<Character> atomicStampedReference = new AtomicStampedReference<>('A',0);
System.out.println(atomicStampedReference.compareAndSet('A', 'B',0,1)
+"\t 当前值="+atomicStampedReference.getReference()+"\t 当前版本号="+atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet('B', 'A',1,2)
+"\t 当前值="+atomicStampedReference.getReference()+"\t 当前版本号="+atomicStampedReference.getStamp());
//以上两步已经形成了一个ABA问题,此时下面的第三步操作由于版本号不是我们想要的0而比较失败
System.out.println(atomicStampedReference.compareAndSet('A', 'C',0,2)
+"\t 当前值="+atomicStampedReference.getReference()+"\t 当前版本号="+atomicStampedReference.getStamp());
}
}
- - 打印结果- - - - - - - - - - - - - - - - -
true 当前值=B 当前版本号=1
true 当前值=A 当前版本号=2
false 当前值=A 当前版本号=2
AtomicInteger可以引发一系列的知识点,如:
CAS------>Unsafe------>CAS底层原理------>ABA问题------>原子引用更新------>如何规避ABA问题
理清思路,便于理解和记忆