在面对并发的场景,我们要对共享的资源进行保护,方式一般有两种,一种是使用Synchronized对资源进行加锁,另外一种方式就是本文要介绍的使用CAS来对共享资源进行保护。
CAS全称是Compare And Swap,意思是比较与交换。通过比较之前的值是否发生改变,来决定是否对共享资源进行修改,如果这个值变了,那就说明有其它线程已经修改过这个值了,则修改失败,返回false,如果值没变,那顺利修改并且返回true
CAS底层是调用了native本地函数库,调用的是c++编写的函数,通过操作系统底层实现CAS的原子性。
Compare,必须需要读这个资源当前的值,才能进行比较,这也就是为什么CAS底层必须需要volatile来帮助。volatile原理
经典的多线程取钱问题就可以用CAS无锁编程解决
interface Account {
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
List ts = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance()
+ " cost: " + (end-start)/1000_000 + " ms");
}
}
class AccountSafe implements Account {
private AtomicInteger balance;
public AccountSafe(Integer balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
while (true) {
int prev = balance.get();
int next = prev - amount;
if (balance.compareAndSet(prev, next)) {
break;
}
}
// 可以简化为下面的方法
// balance.addAndGet(-1 * amount);
}
}
public static void main(String[] args) {
Account.demo(new AccountSafe(10000));
}
相对于加锁,CAS不需要进行消耗繁重的上下文切换,这种上下文切换对于CPU来说消耗是比CAS轮询来的更大的。CAS对于多核CPU来说意义更大一些,多核CPU可以做到真正意义上的并行,一个CPU多执行几次轮询会比执行一次上下文切换所需要的资源少的多。
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
在使用CAS解决并发问题的时候,两个线程先后访问共享资源,两个线程读到的值都是A,然后线程1先执行修改,将A改成了B,之后又将B改成了A,然后到线程2进行CAS操作,这个时候按照CAS的原则,应该是要比较失败的。可是结果还是对的,线程2任然会修改这个值。导致CAS失效。
加一个版本号,变量每修改一次,就将版本号自增一次,然后比较的时候再根据实时的版本来判断是否需要修改这个值。
Java中可以使用AtomicStampedReference来对变量进行修饰
使用方法:
static AtomicStampedReference ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
String prev = ref.getReference();
// 获取版本号
int stamp = ref.getStamp();
log.debug("版本 {}", stamp);
// 如果中间有其它线程干扰,发生了 ABA 现象
other();
sleep(1);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
}
private static void other() {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t1").start();
sleep(0.5);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t2").start();
}
15:41:34.891 c.Test36 [main] - main start...
15:41:34.894 c.Test36 [main] - 版本 0
15:41:34.956 c.Test36 [t1] - change A->B true
15:41:34.956 c.Test36 [t1] - 更新版本为 1
15:41:35.457 c.Test36 [t2] - change B->A true
15:41:35.457 c.Test36 [t2] - 更新版本为 2
15:41:36.457 c.Test36 [main] - change A->C false