CAS,全称 Compare And Swap (比较与交换),是一种乐观锁,同样是锁相比 synchronized性能却要高不少,因为是 synchronized阻塞的,而CAS是非阻塞的。CAS主要有3个操作数,内存值V,预期的旧值A,需要修改的新值B,可以这样理解:在V的地址把A改成B,当V位置的值与预期的旧值A相同时候,则修改成B,否则不更新。
下面看个图简单理解一下CAS:当线程1和线程2同时操作内存V,线程1想要把内存V的变量值从A(2)改成B(1)而线程2想要把V的变量值从A(2)改成B(3)。假设这个时候是线程1优先抢到资源所以线程1先进行CAS操作,这个时候预期旧值2是相等的则执行了更新,更新完后内存V的变量值就变成1,这个时候线程2才进入比较预期的A值与V中实际的变量值已经不相同了,所以更新失败。
这个图看上去是Compare And Swap 是同时操作,但实际上是分2部执行:1.比较(compare),2.交换(swap),它的原子性是通过硬件实现的,而不是我们java代码实现
java提供的CAS操作类
我们随便找其中一个Atomic类学习
当V的值与A相等则更新成功
public static voidmain(String[] args) {
//定义Integer的CAS类AtomicInteger AI = newAtomicInteger();
//设置初始化值AI.set(1);
//将1替换成2并返回是否替换成功,该函数第一个参数为预期的旧值(A),第二个参数是需要修改的值(B)booleanb = AI.compareAndSet(1, 2);
//打印是否替换成功System.out.println(b);
//打印最新值System.out.println(AI);
}
打印结果:
当V的值与A不相等则更新失败
public static voidmain(String[] args) {
//定义Integer的CAS类AtomicInteger AI = newAtomicInteger();
//设置初始化值AI.set(1);
//将1替换成2 并返回是否替换成功,该函数第一个参数为预期的旧值(A),第二个参数是需要修改的值(B)booleanb = AI.compareAndSet(2, 2);
//打印是否替换成功System.out.println(b);
//打印最新值System.out.println(AI);
}
打印结果:
这2个打印结果的结论也证实了最开始那个图的原理,只有V的变量值与A相同时候,才会修改成B ,
接下来看看原子性
普通的int类型
public classAmIntegerTest implementsRunnable {
private static volatile intI= 0;
public voidrun() {
//每个线程自增100000次,for(inti = 0; i++ < 100000; add()) {}
//线程执行完之后结果System.out.println(Thread.currentThread().getName() + ":"+ I);
}
public static voidadd(){
I++;
}
public static voidmain(String[] args) {
AmIntegerTest ait = newAmIntegerTest();
Thread t1 = newThread(ait);
Thread t2 = newThread(ait);
t1.start();
t2.start();
}
}
打印结果显示,普通的int没有原子性
除非加上synchronized关键字,接下来改造add()添加synchronized其他代码不变
public synchronized static voidadd(){
I++;
}
无论执行多少次最终结果都是200000,可以保证原子性
我们再看看java提供的CAS操作类
public classAmIntegerTest implementsRunnable {
//定义Integer的CAS类private staticAtomicInteger AI= newAtomicInteger();
public voidrun() {
//每个线程自增100000次,for(inti = 0;i++ < 100000; AI.incrementAndGet()) {}
//线程执行完之后结果System.out.println(Thread.currentThread().getName() + ":"+ AI.get());
}
public static voidmain(String[] args) {
AmIntegerTest ait = newAmIntegerTest();
Thread t1 = newThread(ait);
Thread t2 = newThread(ait);
t1.start();
t2.start();
}
}
无论执行多少次最终结果都是200000,所以是具有原子性的,但是它的原子性是其他语言实现的,这里就不讨论它的实现原理了(我不会C++)
AtomicInteger.value是一个volatile修饰的变量(内存锁定,同一时刻只有一个线程可以修改内存值)
private volatile intvalue;
AtomicInteger.incrementAndGet()这是一个自增函数,实现了自旋锁(无限循环),下面看看incrementAndGet()的源代码,它调用了Unsafe 类的getAndAddInt()。而getAndAddInt()是无限循环,直到值修改成功才结束,否则一直循环
public final intincrementAndGet() {
returnunsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
private static finalUnsafe unsafe= Unsafe.getUnsafe();
public final intgetAndAddInt(Object var1, longvar2, intvar4) {
intvar5;
do{
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
returnvar5;
}
getAndAddInt()实现的自旋锁原理就是内存V的变量值与A不一致时候,再重新获取V的变量值,直到V的变量值与A一致时候,才更新成B并结束,这样有个缺点就是如果自旋次数太多,会造成很大的资源消耗
在Atomic包中的CAS操作都是基于以下3个方法实现,Unsafe类里面的所有方法都是 native 声明的,说明是调用其他语言实现的
//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
public final native booleancompareAndSwapObject(Object o, longoffset,Object expected, Object x);
public final native booleancompareAndSwapInt(Object o, longoffset,intexpected,intx);
public final native booleancompareAndSwapLong(Object o, longoffset,longexpected,longx);
我们试试使用一下这些函数
public classCasTest {
//定义java的CAS类Unsafepublic staticUnsafe U= getUnsafe();
public static voidmain(String[] args) throwsNoSuchFieldException {
// java Unsafe类调用C++实现的CAS//创建一个我们的测试对象Cas cas = newCas("1");//获取对象内value存储对象的地址longoffset = U.objectFieldOffset(cas.getClass().getDeclaredField("value"));//通过Unsafe类的CAS函数进行修改System.out.println(U.compareAndSwapObject(cas,offset,"1","2"));System.out.println(cas);}
static classCas{
privateString value;
publicCas(String value) {
this.value= value;}
@OverridepublicString toString() {
return"Cas{"+
"value='"+ value+ '\''+
'}';}
}
//因为Unsafe的构造函数是私有的,而且它提供的getUnsafe()也只有系统类才能使用,//所以我们只能通过反射获取Unsafe实例了private staticUnsafe getUnsafe() {
Unsafe unsafe = null;try{
Constructor> declaredConstructor = Unsafe.class.getDeclaredConstructors()[0];declaredConstructor.setAccessible(true);unsafe = (Unsafe) declaredConstructor.newInstance();} catch(Exception e) { e.printStackTrace();}
returnunsafe;}
}
看一下main()的打印结果,修改cas对象的value值,从“1”改成“2”,执行成功
改一下main(),试一下从“2”改成“2”
public static voidmain(String[] args) throwsNoSuchFieldException {
// java Unsafe类调用C++实现的CAS//创建一个我们的测试对象Cas cas = newCas("1");//获取对象内value存储对象的地址longoffset = U.objectFieldOffset(cas.getClass().getDeclaredField("value"));//通过Unsafe类的CAS函数进行修改System.out.println(U.compareAndSwapObject(cas,offset,"2","2"));System.out.println(cas);}
这个时候修改是失败的,因为value的值为“1”,而CAS操作里 预期的旧值是“2”,所以无法成功执行从“2”改成“2”
下面2个函数都是Unsafe类里面的,都是私有的,而且它提供的getUnsafe()也只有系统类才能使用,所以需要通过反射获取实例
privateUnsafe() {
}
@CallerSensitive
public staticUnsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw newSecurityException("Unsafe");} else{
returntheUnsafe;}
}
ABA问题
ABA就是内存V的变量值从A变成B,再从B变成A,这个时候CAS只判断值是否相等,只要值相等就会认为这个值没改变过,但实际上是已经变化了。我们可以添加多一个标识(版本号、时间)判断这个值是否已经改变过,java也提供了相关的解决方案 AtomicStampedReference 类。
先看一下普通CAS的操作类
public static voidmain(String[] args) {
//定义Integer的CAS类AtomicInteger AI = newAtomicInteger(1);
System.out.println("初始化值:"+ AI);
AI.compareAndSet(1,2);
System.out.println("第一次CAS操作完成之后值:"+ AI);
AI.compareAndSet(2,1);
System.out.println("第二次CAS操作完成之后值:"+ AI);
AI.compareAndSet(1,3);
System.out.println("第二次CAS操作完成之后值:"+ AI);
}
这种只需要V的值与A一致就可以修改,存在ABA问题
接下来看看CAS的标识引用类
public static voidmain(String[] args) {
//定义AtomicStampedReference类,第一个参数是我们的初始化值,第二个是版本标识AtomicStampedReference ASR = newAtomicStampedReference(1,1);
System.out.println("初始化值:"+ ASR.getReference());
ASR.compareAndSet(1,2, ASR.getStamp(), ASR.getStamp() + 1);
System.out.println("第一次CAS操作完成之后值:"+ ASR.getReference());
//获取stamp ,下面2次新增都使用这个stamp值intstamp = ASR.getStamp();
//这一次可以修改成功,因为stamp的预期值一致ASR.compareAndSet(2,1, stamp, stamp + 1);
System.out.println("第二次CAS操作完成之后值:"+ ASR.getReference());
//这一次可以修改失败,因为stamp的预期值不一致ASR.compareAndSet(1,3, stamp, stamp + 1);
System.out.println("第三次CAS操作完成之后值:"+ ASR.getReference());
}
这种不紧V的值要与A相等,且记录的标识也需要一致才能完成修改
CAS是乐观锁,synchronized是悲观锁。两者各有优异,应对不同场景选择合适的锁。