深入理解CAS
我们知道volatile
是不能保证原子性的,在这这篇文章中我们用了一个叫AtomicInteger
类来解决这个原子性的问题。那行,这些我们就来仔细看看CAS到底是啥玩意~~~
什么是CAS?
顾名思义,CAS
(compareAndSet()
)就是比较并交换
我们下面来个demo
:
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
/*
compareAndSet(希望与主内存的值,自己要更改的值)
*/
System.out.println(atomicInteger.compareAndSet(5, 2091) + "\t" + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 3001) + "\t" + atomicInteger.get());
}
}
/*
true 2091
false 2091
*/
通过上述代码注释compareAndSet()
的参数含义和运行结果我们都可以猜出:这个方法的意思是假如我的预期值和主存中的值是一样的(都为5),那么我就将该值改变为2091。
所以为什么第一个是true
第二个为什么为false
了:因为第一次改了之后,你的主存的值就不再是5了,而是2091了,所以就不匹配了,就不能改变新的值了。
剖析CAS
我们知道假上一节说到volatile
的时候我们用了 atomicInteger.getAndIncrement()
操作来保证了原子性。
那么,这个AtomicInteger
类究竟有啥魔力可以让CAS
这么听话地保证了原子性?
那么我们就来看看这个 getAndIncrement()
源码的方法究竟是啥?
getAndIncrement()
方法:参数解释(this:当前对象,valueOfferset:就是地址偏移量,1:就是你所要自增的数值)
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
unsafe
类中的getAndAddInt()
方法:参数解释跟上面无异,只不过这里需要知道这里用到的是自旋的do{} while()
操作
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
// 每次都根据对象和地址偏移量去主存拿数值 赋值给v
v = getIntVolatile(o, offset);
// 如果期望值o和当前v值是一样的,就进行更新值v + delta操作
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
compareAndSwapInt()
方法:底层用到的就是该CAS
的思想,靠的是CPU指令并发原语来保证线程安全性的。
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
所以上述流程解释为:(这算是第二次解释了)假设线程A和B两个线程同时执行getAndAddInt
操作:
AtomicInteger
里面的value
原始值为3,即主内存中AtomicInteger
的value
为3,根据JMM内存模型,线程A和线程B各自持拷贝了value
为3的副本到自己的工作内存了;- 线程A通过
getIntVolatile(var 1,var 2)
获取到value
为3,然后被挂起; - 线程B也通过
getIntVolatile(var 1,var 2)
获取到value
为3,但是没有被挂起,然后线程B就开始执行compareAndSwapInt()
比较发现自己工作内存中的值和主内存的值比较是一样的,就将数值改为4,然后线程B收工; - 线程A恢复,执行
compareAndSwapInt()
发现自己的工作内存的值和主内存的值4不一致了,说明该值已经被其他线程修改了,所以线程A修改失败,只能重新读取重来一遍了。
那CAS有啥缺点呢?
-
如果
CAS
失败,会一直进程尝试,如果CAS
长时间一直不成功,就可能会给CPU
带来很大的开销。 -
只能保证一个共享变量的原子操作;( v是不是代表一个?)
public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; }
-
会出现ABA问题;
什么是ABA问题?
直接看图:
下面我们直接看一个demo
:
public class AtomicStampReference {
public static void main(String[] args) throws InterruptedException {
System.out.println("===========以下是ABA问题的产生===========");
AtomicInteger atomicInteger = new AtomicInteger(10);
new Thread(() -> {
System.out.println(atomicInteger.compareAndSet(10, 100));
System.out.println(atomicInteger.compareAndSet(100, 10));
System.out.println(atomicInteger.compareAndSet(10, 100));
}, "t1").start();
new Thread(() -> {
System.out.println(atomicInteger.compareAndSet(100, 10) + " " + atomicInteger.get());
}, "t2").start();
Thread.sleep(2000);
}
}
上述的代码运行起来是没问题的,证明CAS
确实没办法解决这个ABA问题(中间线程已经将主内存操作很多次了,只是最后的时候的那个值还是和第一个值一样,所以要规避这个问题),那么如何解决呢?
如何解决ABA问题?
这里我们需要认识一个新的知识:原子引用 AtomicReference
下面来个AtomicReference
类的demo
:
class User{
int age;
String name;
public User(String name,int age) {
this.name = name;
this.age = age;
}
get()/set()。。。
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User z3 = new User("z3", 23);
User li4 = new User("li4",24);
AtomicReference<User> reference = new AtomicReference<>();
reference.set(z3);
System.out.println(reference.compareAndSet(z3, li4) + " " + reference.get().toString());
System.out.println(reference.compareAndSet(z3, li4) + " " + reference.get().toString());
}
}
/*
true User{age=24, name='li4'}
false User{age=24, name='li4'}
*/
玩法就是跟AtomicInteger
类相似。
延申出来,如何解决这个ABA
问题呢?就需要用到了时间戳引用,即修改版本号 AtomicStampReference
。
public class AtomicStampReference {
public static void main(String[] args) throws InterruptedException {
System.out.println("===========以下是ABA问题的解决===========");
new Thread(() -> {
try {
// 获取时间戳
int stamp = stampedReference.getStamp();
stampedReference.compareAndSet(10, 100, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t第一次获取的版本号:" + stamp);
Thread.sleep(1000);
stampedReference.compareAndSet(100, 10, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第二次获取的版本号:" + stampedReference.getStamp());
stampedReference.compareAndSet(10, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第三次获取的版本号:" + stampedReference.getStamp());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t3").start();
new Thread(() -> {
try {
// 获取时间戳
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第一次获取的版本号:" + stamp);
Thread.sleep(3000);
boolean result = stampedReference.compareAndSet(100, 10, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t第二次获取的版本号:" + stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t修改成功否:" + result);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t4").start();
}
}
/*
===========以下是ABA问题的解决===========
t3 第一次获取的版本号:1
t4 第一次获取的版本号:2
t3 第二次获取的版本号:3
t3 第三次获取的版本号:4
t4 第二次获取的版本号:41
t4 修改成功否:false
*/
综上,AtomicStampReference
就可以解决ABA
问题啦~