cas顾名思义是比较交换,实现了并发安全特性的原子性,是基于硬件平台的汇编指令,也就是说基于硬件实现的,通过比较预期的值是否和内存中值是否一致,如果一致则更新,如不一致则重新获取内存的值进行比较。和其他实现原子性的方式不一样的是此方式不需要加锁,大大提高了执行效率,解决了加锁释放锁导致的上下文切换的问题。
以AtomicInteger为例,此原子类是并发安全的,内部是通过CAS和voliate实现的。如何证明此AtomicInteger是并发安全的呢,举个例子如下:利用100个线程对i进行加1操作,无论允许多少次最后得到结果就是100。为什么呢?内部怎么实现的?
private static AtomicInteger i = new AtomicInteger(0);
public static void main(String[] args) {
for (int i=0;i<100;i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
add();
}
}).start();
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(i.get());
}
public static void add() {
i.addAndGet(1);
}
我们看一下内部源码:
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
使用unsafe类,unsafe类是java中的原子类, unsafe类中基本都是native方法,native方法底层通过JNI调用c的方法库了,至此java是看不到的。而getAndAddInt方法中的逻辑是什么呢?
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public native int getIntVolatile(Object var1, long var2);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
以上图中,各个参数的含义:
var1: 是AtomicInteger对象
var2:下图AtomicInteger源码中valueoffset,表示value的引用地址
var5:getIntVolatile(var1,var2) 通过此方法获取的值,我们从上一讲java之voliate中知道,每个线程对于共享对象的修改操作,并非直接修改内存,而是将主内存的数据,读取到自己的工作内存中,而var5则是工作内存的value的值
var4:表示需要增加的值
var5+var4:更新的值
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); }
}
private volatile int value;
基本原理:
var5为工作内存的值,调用compareAndSwapInt方法后,底层通过比较var1对象中的value值和var5进行比较,为什么是var1对象中的value值? 我们可以看到上图中的value声明成为了voliate,我们知道,当设置voliate之后,value是具有可见性的,当主内存修改后,每个线程的value则会失效,并且重新读取主内存的值,所以此value也可以说明是主内存的值。比较之后,如果一致,说明没有被修改,则将值更新为var5+var4,如果不一致,则进入下个循环重新读取工作内存的值,直到数据一致。
举个并发例子说明其原理:
线程A和线程B同时对上图中的i进行add操作:
(1)当线程A执行到getIntVolatile后,var5拿到的工作内存中的值为0,线程A被挂起
(2)此时线程B执行到getIntVolatile,并且var5也为0,然后执行compareAndSwapInt操作,比较主内存和工作内存的值为0,数据一致,则value值被更新为1。
(3)线程A拿到CPU执行权,然后执行compareAndSwapInt操作,判断var5和value不一致,说明此value被其他线程修改了,进入下一个循环。直到成功
CAS内部使用自旋的方式进行CAS更新(while循环进行CAS更新,如果更新失败,则循环再次重试)
(1)由于是自旋操作,假如工作线程的值和主线程值一直不相等,则陷入死循环中,增大了CPU的开销
(2)ABA的问题,假如线程A,对value进行了+1,然后又对value进行-1的操作,实际上value值是没有变化的。怎么解决此问题?可以添加版本号,当value+1后,版本号也随之+1,然后-1后,版本号也随之+1,compareAndSet判断时不仅仅需要判断值是否相等还需要判断版本号是否相等。JDK的Atomic包里提供了一个类AtomicStampedReference也是此原理。
(3)CAS仅仅只能保证一个共享变量的原子性,不能保证多个变量的原子性。相应实现多个变量的原子性,可以将多个变量封装到一个对象中,然后使用AtomicStampedReference来操作此对象的更新。