CAS机制
在说CAS之前,先让我们看一个简单的多线程例子:
public class Test {
public static int num = 0;
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
}catch (Exception e){
}
for (int j=0;j<50;j++){
num++;
}
}
}).start();
}
try {
Thread.sleep(1000);
}catch (Exception e){
}
System.out.println(num);
}
}
我们启动3个线程,每个线程里让count变量自增50次,按照正常情况最后打印出的num应该是3*50=150,但是在多线程情况下我们得到的不一定是150。
这个是因为num++操作不是原子性的,这样在多线程情况下就会出现问题,遇到这种情况我们最先想到的就是加锁:synchronized,最终我们每次执行都会得到正确结果150,如下代码:
for (int j=0;j<50;j++){
synchronized (Test.class){
num++;
}
}
但是我们都知道synchronized采用的是悲观锁,也就是只能有一个线程能访问到当前代码块,其他没有获取到锁的线程都会在block状态,知道获取到锁,然后转成runable状态,代价比较高。
其实针对于这个例子我们可以采用一种更优的方式来实现:concurrent并发包里的原子类-AtomicInteger,下面我们用AtomicInteger来实现以下上面的功能:
public class Test {
public static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
}catch (Exception e){
}
for (int j = 0; j < 50; j++) {
num.incrementAndGet();
}
}
}).start();
}
try {
Thread.sleep(1000);
}catch (Exception e){
}
System.out.println(num);
}
}
运行上面的代码,我们发现每次都是我们预期的值:为什么会有这种效果呢,真实因为原子类的底层采用的就是CAS机制。
CAS:是Compare And Swap的缩写,意思是比较并且替换。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
我们看一下incrementAndGet方法:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
我们发现底层其实调用的是:unsafe的compareAndSwapInt方法。
CAS也有自己的缺点:由于采用的是乐观锁策略,当命中的时候他会一直重试,这样的话会消耗大量CPU资源,并且CAS只能用于单个变量的原子性,不可以采用在多变量同时需要原子性操作的场景,最大的一个问题是CAS会存在ABA问题。
ABA:一个变量的值从A变成了B,又从B变成了A。
假如有个变量的值为A,我们有3个线程同时操作:1.把A变成B 期望最后值为B
2.把A变成B 期望最后值为B
3.期望最后值为A
假如,线程1先来,把A改成了B,线程2被阻塞了,线程3又把值改成了A,这个时候线程2恢复的话,就会把值改成B,但是按我们的要求应该是无法修改的,因为获取到的A已经不是之前的A了,是被修改过的值。举个例子
此时操作一个单向链表堆栈,栈顶为A,A.next=B,线程1想将栈顶A换成B,在这个时候线程2来了,把A,B都pop出去了,然后重新push进了C,D,A,此时B处于游离状态,这个时候线程1开始修改栈顶,发现栈顶还是A,符合CAS原则,把A换成了B,但是由于B的next是null,所以无缘无故的就把C,D给丢掉了。
ABA解决办法:版本号比较,在进行预期值A和实际值作比较的时候,加上一个版本号的比较,每次操作的时候版本号都会+1,只有同时符合这两个条件才可以进行操作。
AtomicStampedReference类就实现了这个版本号比较功能:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
举个例子:
public class ABATest {
private static AtomicInteger num = new AtomicInteger(10);
private static AtomicStampedReference num2 = new AtomicStampedReference(10,0);
public static void main(String[] args) {
casAtomicInteger();
casAtomicStampedReference();
}
public static void casAtomicInteger(){
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
num.compareAndSet(10,11);
num.compareAndSet(11,10);
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
}catch (Exception e){
}
boolean flag = num.compareAndSet(10,11);
System.out.println(flag);
}
});
thread1.start();
thread2.start();
}
public static void casAtomicStampedReference(){
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
}catch (Exception e){
}
num2.compareAndSet(10,11,num2.getStamp(),num2.getStamp()+1);
num2.compareAndSet(11,10,num2.getStamp(),num2.getStamp()+1);
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = num2.getStamp();
System.out.println("before stamp=="+stamp);
try {
Thread.sleep(100);
}catch (Exception e){
}
System.out.println("after stamp=="+num2.getStamp());
boolean flag2 = num2.compareAndSet(10,11,stamp,stamp+1);
System.out.println(flag2);
}
});
thread1.start();
thread2.start();
}
}
结果: