CAS无锁编程详解

概述

在面对并发的场景,我们要对共享的资源进行保护,方式一般有两种,一种是使用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呢?

相对于加锁,CAS不需要进行消耗繁重的上下文切换,这种上下文切换对于CPU来说消耗是比CAS轮询来的更大的。CAS对于多核CPU来说意义更大一些,多核CPU可以做到真正意义上的并行,一个CPU多执行几次轮询会比执行一次上下文切换所需要的资源少的多。

CAS的特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

  • CAS是基于乐观锁的思想,我很乐观,觉得这个资源不会被其它线程修改,我不加锁直接去修改,然后发现值不对,大不了我重新执行一下操作。
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想 改,我改完了解开锁,你们才有机会。

ABA 问题及解决

什么是ABA问题

在使用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 

你可能感兴趣的:(JUC学习以及源码分析,juc)