一、CAS是什么
cas
全称compare and swap
,比较并交换,是一条CPU并发原语。解决多线程环境下使用锁导致上下文切换导致性能消耗的一种机制。它的功能是判断内存中某个地址的值是否是期望的值,如果是就修改为新的值,整个过程是原子的。
这是一种非阻塞算法,线程在获取资源失败时,不需要挂起,因此省去了上下文切换带来的性能损耗。
CAS并发原语体现在java中就是Unsafe
类中的方法。调用Unsafe
类中的方法,JVM会自动实现出CAS汇编指令。
二、CAS原理
首先先来看一段代码。
public class CasTest {
public static void main(String[] args) {
// num 初始值 = 5
AtomicInteger num = new AtomicInteger(5);
num.getAndIncrement();
}
}
在讲解volatile
的那篇文章中,为了解决多线程环境下i++
操作不能同步的情况,使用了原子类AtomicInteger
。那么为什么这个类能够解决这个问题呢?它底层是怎么处理的呢?
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
可以看到,它实际上调用了unsafe
类的getAndAddInt
方法,并给了一个步长1。
在接着看Unsafe
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;
}
在底层实际上采用了自旋锁的思想。如果比较成功就更新值,如果失败就一直重试,直到成功。
三、CAS缺点
- 循环时间长开销大
- 只能保证一个共享变量的原子性
- 带来ABA问题
四、ABA问题
前面两个缺点都很好理解。那么什么是ABA问题捏?
什么是ABA问题
在讲voaltile
的时候讲过了JMM内存模型,如果有两个线程同时修改内存中的一个共享变量,A线程修改需要10秒,B线程修改需要2秒,第一次B线程修改结束后,A线程还没有结束,这时B线程又将内存中的值改回来了。导致A线程在10秒后也修改了内存的值。这就是ABA问题。还是来看看代码:
package com.fzkj.juc.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @DESCRIPTION 演示ABA问题
*/
public class ABADemo {
public static void main(String[] args) {
aba();
}
public static void aba(){
AtomicInteger num = new AtomicInteger(5);
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
num.compareAndSet(5, 97);
System.out.println("AAA线程将num的值修改为了 -> " + num);
}, "AAA").start();
new Thread(() -> {
num.compareAndSet(5, 98);
System.out.println("BBB线程将num的值修改为了 -> " + num);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 1秒后BBB线程将值修改回来
num.compareAndSet(98, 5);
System.out.println("BBB线程将num的值修改为了 -> " + num);
}, "BBB").start();
try {
TimeUnit.SECONDS.sleep(8);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("num 最后的值是 -> " + num);
}
}
执行结果:
BBB线程将num的值修改为了 -> 98
BBB线程将num的值修改为了 -> 5
AAA线程将num的值修改为了 -> 97
num 最后的值是 -> 97
这就是ABA问题。虽然结果没问题,但是过程却不对。
ABA问题解决
AtomicReference原子引用
package com.fzkj.juc.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* @DESCRIPTION 演示ABA问题
*/
public class ABADemo {
public static void main(String[] args) {
// aba();
reference();
}
public static void reference(){
User u1 = new User("1" ,"name1");
User u2 = new User("2", "name2");
AtomicReference userReference = new AtomicReference<>();
userReference.set(u1);
// 比较并交换
boolean b = userReference.compareAndSet(u1, u2);
System.out.println(b + "\t d当前值是 -> " + userReference.get().toString());
System.out.println(userReference.compareAndSet(u1, u2));
}
static class User {
String id;
String name;
public User(String id, String name){
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "User [id = " + id + ", name = " + name + "]";
}
}
}
// 运行结果
true d当前值是 -> User [id = 2, name = name2]
false
AtomicStampedReference
package com.fzkj.juc.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @DESCRIPTION 演示ABA问题
*/
public class ABADemo {
public static void main(String[] args) {
// aba();
// reference();
ABAResolve();
}
public static void ABAResolve(){
AtomicStampedReference<Integer> num = new AtomicStampedReference<>(5, 1);
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num.compareAndSet(5, 97, 1, 2));
System.out.println("AAA线程将num的值修改为了 -> " + num.getReference().toString());
}, "AAA").start();
new Thread(() -> {
System.out.println(num.compareAndSet(5, 98, 1, 2));
System.out.println("BBB线程将num的值修改为了 -> " + num.getReference().toString());
}, "BBB").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("num 最后的值是 -> " + num.getReference().toString());
}
}
运行结果:
true
BBB线程将num的值修改为了 -> 98
false
AAA线程将num的值修改为了 -> 98
num 最后的值是 -> 98
使用AtomicStampedReference
类,可以有效的解决ABA带来的问题,在创建这个类的时候会要求传一个期望的stamp
,可以理解为版本号,后续会拿版本号进行比较,如果版本号相同才会修改。