• 案例

使用CAS无锁方式保证银行转账业务线程安全,CAS的核心就是自旋重试,直到结果正确。

  1. public class TestCAS {
  2. public static void main(String[] args) {
  3. Account account = new AccountCas(10000);
  4. Account.demo(account);
  5. }
  6. }
  7. class AccountCas implements Account {
  8. private AtomicInteger balance;
  9. public AccountCas(int balance) {
  10. this.balance = new AtomicInteger(balance);
  11. }
  12. @Override
  13. public Integer getBalance() {
  14. return balance.get();
  15. }
  16. @Override
  17. public void withdraw(Integer amount) {
  18. while (true) {
  19. // 获取余额的最新值
  20. int prev = balance.get();
  21. // 要修改的余额
  22. int next = prev - amount;
  23. // 真正修改
  24. if (balance.compareAndSet(prev, next)) {
  25. break;
  26. }
  27. }
  28. // balance.getAndAdd(-1 * amount);
  29. }
  30. }
  31. interface Account {
  32. // 获取余额
  33. Integer getBalance();
  34. // 取款
  35. void withdraw(Integer amount);
  36. /**
  37. * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
  38. * 如果初始余额为 10000 那么正确的结果应当是 0
  39. */
  40. static void demo(Account account) {
  41. List<Thread> ts = new ArrayList<>();
  42. for (int i = 0; i < 1000; i++) {
  43. ts.add(new Thread(() -> {
  44. account.withdraw(10);
  45. }));
  46. }
  47. long start = System.nanoTime();
  48. ts.forEach(Thread::start);
  49. ts.forEach(t -> {
  50. try {
  51. t.join();
  52. } catch (InterruptedException e) {
  53. e.printStackTrace();
  54. }
  55. });
  56. long end = System.nanoTime();
  57. System.out.println(account.getBalance() + " cost: " + (end - start) / 1000_000 + " ms");
  58. }
  59. }

CAS 与 volatile

CAS

CAS无锁保证线程安全,其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作(硬件层面实现)。

  1. public void withdraw(Integer amount) {
  2. while (true) {
  3. // 需要不断尝试,直到成功为止
  4. while (true) {
  5. // 比如拿到了旧值 1000
  6. int prev = balance.get();
  7. // 在这个基础上 1000-10 = 990
  8. int next = prev - amount;
  9. /*
  10. compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值
  11. - 不一致了,next 作废,返回 false 表示失败
  12. 比如,别的线程已经做了减法,当前值已经被减成了 990
  13. 那么本线程的这次 990 就作废了,进入 while 下次循环重试
  14. - 一致,以 next 设置为新值,返回 true 表示成功
  15. */
  16. if (balance.compareAndSet(prev, next)) {
  17. break;
  18. }
  19. }
  20. }
  21. }

image.png

  1. 线程1在修改余额从100->90时,线程2已经将余额改为90了
  2. 线程1在进行cas操作时发现,期望值是100实际值是90,则本次cas操作失败,重新重主存中获取值90,再次进行cas操作
  3. 线程1根据主存中的90自旋一次为80后,线程2又改变了主存中的值为80,线程1本次cas操作又失败,继续自旋
  4. 线程1获取主存值80,进行计算,此时没有其他线程在操作,线程1本次cas操作成功

volatile

获取共享变量时,为了保证该变量的可见性,需要使用 volatile修饰。它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile变量都是直接操作主存。即一个线程对 volatile变量的修改,对另一个线程可见。
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

为什么无锁效率高

无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。

CAS特点

结合 CASvolatile可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,可以再重试。
synchronized是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
CAS体现的是无锁并发、无阻塞并发。

CAS的应用

原子整数

J.U.C并发包提供了:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong
    AtomicInteger为例

    1. AtomicInteger i = new AtomicInteger(0);
    2. // 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
    3. System.out.println(i.getAndIncrement());
    4. // 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
    5. System.out.println(i.incrementAndGet());
    6. // 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
    7. System.out.println(i.decrementAndGet());
    8. // 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
    9. System.out.println(i.getAndDecrement());
    10. // 获取并加值(i = 0, 结果 i = 5, 返回 0)
    11. System.out.println(i.getAndAdd(5));
    12. // 加值并获取(i = 5, 结果 i = 0, 返回 0)
    13. System.out.println(i.addAndGet(-5));
    14. // 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
    15. // 其中函数中的操作能保证原子,但函数需要无副作用
    16. System.out.println(i.getAndUpdate(p -> p - 2));
    17. // 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
    18. // 其中函数中的操作能保证原子,但函数需要无副作用
    19. System.out.println(i.updateAndGet(p -> p + 2));
    20. // 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
    21. // 其中函数中的操作能保证原子,但函数需要无副作用
    22. // getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
    23. // getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
    24. System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
    25. // 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
    26. // 其中函数中的操作能保证原子,但函数需要无副作用
    27. System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));

    原子引用

    基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用引用类型原子类。

  • AtomicReference:引用类型原子类

  • AtomicStampedReference:原子更新引用类型里的字段原子类
  • AtomicMarkableReference:原子更新带有标记位的引用类型
  • 案例 ```java public class Test35 { public static void main(String[] args) {
    1. DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000")));
    } }

class DecimalAccountCas implements DecimalAccount { //保护BigDecimal对象的原子操作 private AtomicReference balance;

  1. public DecimalAccountCas(BigDecimal balance) {
  2. this.balance = new AtomicReference<>(balance);
  3. }
  4. @Override
  5. public BigDecimal getBalance() {
  6. return balance.get();
  7. }
  8. @Override
  9. public void withdraw(BigDecimal amount) {
  10. while(true) {
  11. //对BigDecimal对象里的值进行原子保护
  12. BigDecimal prev = balance.get();
  13. BigDecimal next = prev.subtract(amount);
  14. if (balance.compareAndSet(prev, next)) {
  15. break;
  16. }
  17. }
  18. }

}

interface DecimalAccount { // 获取余额 BigDecimal getBalance();

  1. // 取款
  2. void withdraw(BigDecimal amount);
  3. /**
  4. * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
  5. * 如果初始余额为 10000 那么正确的结果应当是 0
  6. */
  7. static void demo(DecimalAccount account) {
  8. List<Thread> ts = new ArrayList<>();
  9. for (int i = 0; i < 1000; i++) {
  10. ts.add(new Thread(() -> {
  11. account.withdraw(BigDecimal.TEN);
  12. }));
  13. }
  14. ts.forEach(Thread::start);
  15. ts.forEach(t -> {
  16. try {
  17. t.join();
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. });
  22. System.out.println(account.getBalance());
  23. }

}

  1. <a name="HEGB3"></a>
  2. ### ABA 问题及解决
  3. - **ABA问题复现**
  4. ```java
  5. /**
  6. * @author SongHongWei
  7. * 模拟ABA问题,CAS中的
  8. * @date 2021/10/3
  9. */
  10. @Slf4j
  11. public class ABAProblem {
  12. static AtomicReference<String> ref = new AtomicReference<>("A");
  13. public static void main(String[] args) {
  14. log.info("main start...");
  15. String prev = ref.get();
  16. aba();
  17. sleep(1);
  18. log.info("change A->C...{}", ref.compareAndSet("A", "C"));
  19. }
  20. /**
  21. * @description 线程1 先将共享变量从A->B,线程B再将共享变量B->A
  22. * @author SongHongWei
  23. */
  24. private static void aba() {
  25. new Thread(() -> {
  26. boolean flag = ref.compareAndSet("A", "B");
  27. log.info("change A->B...{}", flag);
  28. }, "t1").start();
  29. sleep(0.5);
  30. new Thread(() -> {
  31. boolean flag = ref.compareAndSet("B", "A");
  32. log.info("change B->A...{}", flag);
  33. }, "t2").start();
  34. }
  35. }
  • 分析

主线程仅能判断出共享变量的值与最初值 A是否相同,不能感知到这种从 A改为 B又改回 A的情况,如果主线程希望:只要有其它线程【动过了】共享变量,那么自己的 cas就算失败,这时,仅比较值是不够的,需要再加一个版本号

AtomicStampedReference

AtomicStampedReference可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A ``-``> B ``-``> A ``-``>C ,通过AtomicStampedReference,可以知道,引用变量中途被更改了几次。

  1. static AtomicStampedReference<String> sref = new AtomicStampedReference<>("A", 0);
  2. public static void main(String[] args) {
  3. log.info("main start...");
  4. atomicSref();
  5. }
  6. private static void atomicSref() {
  7. //获取值
  8. String s = sref.getReference();
  9. //获取版本号
  10. int stamp = sref.getStamp();
  11. saba();
  12. sleep(1);
  13. //执行失败,因为版本号被其他线程修改了
  14. log.info("change A->C...{},现在的版本号{}", sref.compareAndSet(s, "C", stamp, stamp + 1), sref.getStamp());
  15. }
  16. private static void saba() {
  17. new Thread(() -> {
  18. int stamp = sref.getStamp();
  19. boolean flag = sref.compareAndSet(sref.getReference(), "B", stamp, stamp + 1);
  20. log.info("change A->B...{}", flag);
  21. }, "t1").start();
  22. sleep(0.5);
  23. new Thread(() -> {
  24. int stamp = sref.getStamp();
  25. boolean flag = sref.compareAndSet(sref.getReference(), "A", stamp, stamp + 1);
  26. log.info("change B->A...{}", flag);
  27. }, "t2").start();
  28. }

AtomicMarkableReference

AtomicStampedReference可以记录变量被修改过多少次,但是有时候,程序并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference

  • 案例

保洁阿姨与主人共用一个垃圾袋,垃圾袋满了可以换个垃圾袋,也可以倒掉垃圾继续使用垃圾打,只要垃圾袋是空的就不需要更换垃圾袋。
image.png

  1. public class ABAProblem {
  2. public static void main(String[] args) throws InterruptedException {
  3. GarbageBag bag = new GarbageBag("装满了垃圾");
  4. // 参数2 mark 可以看作一个标记,表示垃圾袋满了
  5. AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
  6. log.debug("start...");
  7. GarbageBag prev = ref.getReference();
  8. log.debug(prev.toString());
  9. //保洁线程倒掉垃圾
  10. new Thread(() -> {
  11. log.debug("start...");
  12. bag.setDesc("空垃圾袋");
  13. ref.compareAndSet(bag, bag, true, false);
  14. log.debug(bag.toString());
  15. }, "保洁阿姨").start();
  16. sleep(1);
  17. log.debug("想换一只新垃圾袋?");
  18. boolean success = ref.compareAndSet(prev, new GarbageBag("新垃圾袋"), true, false);
  19. log.debug("换了么?" + success);
  20. log.debug(ref.getReference().toString());
  21. }
  22. }
  23. class GarbageBag {
  24. String desc;
  25. public GarbageBag(String desc) {
  26. this.desc = desc;
  27. }
  28. public void setDesc(String desc) {
  29. this.desc = desc;
  30. }
  31. @Override
  32. public String toString() {
  33. return super.toString() + " " + desc;
  34. }
  35. }