• Java中 synchronizedReentrantLock 等 独占锁就是 **悲观锁** 思想的体现 。
  • 在Java中java.util.concurrent.atomic包下面的原子变量类就是使用基于乐观锁思想的 **CAS** 方式实现的 。
  • 管程即monitor阻塞式的悲观锁实现并发控制,这章我们将通过非阻塞式的乐观锁的来实现并发控制

问题引出

使用synchronized等加锁的方式实现线程安全

  • 有如下需求,保证account.withdraw取款方法的线程安全, 下面使用synchronized保证线程安全

Account接口中有两个接口方法,跟一个测试方法,

  1. interface Account {
  2. // 获取余额
  3. Integer getBalance();
  4. // 取款
  5. void withdraw(Integer amount);
  6. /**
  7. * Java8之后接口新特性, 可以添加默认方法
  8. * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
  9. * 如果初始余额为 10000 那么正确的结果应当是 0
  10. */
  11. static void demo(Account account) {
  12. List<Thread> ts = new ArrayList<>();
  13. long start = System.nanoTime();
  14. for (int i = 0; i < 1000; i++) {
  15. ts.add(new Thread(() -> {
  16. account.withdraw(10);
  17. }));
  18. }
  19. ts.forEach(thread -> thread.start());
  20. ts.forEach(t -> {
  21. try {
  22. t.join();
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. });
  27. long end = System.nanoTime();
  28. System.out.println(account.getBalance()
  29. + " cost: " + (end - start) / 1000_000 + " ms");
  30. }
  31. }

实现

  1. class AccountUnsafe implements Account {
  2. private Integer balance;
  3. public AccountUnsafe(Integer balance) {
  4. this.balance = balance;
  5. }
  6. @Override
  7. public Integer getBalance() {
  8. synchronized (this) {
  9. return balance;
  10. }
  11. }
  12. @Override
  13. public void withdraw(Integer amount) {
  14. // 通过这里加锁就可以实现线程安全,不加就会导致线程安全问题
  15. synchronized (this) {
  16. balance -= amount;
  17. }
  18. }
  19. }

测试
demo方法是静态的,需要接口来调,因为它就是声明在接口的。

  1. public class Test1 {
  2. public static void main(String[] args) {
  3. Account.demo(new AccountUnsafe(10000));
  4. }
  5. }

无锁的方式实现线程安全

上面的代码中使用synchronized加锁操作来保证线程安全,但是 synchronized加锁操作太耗费资源 (因为底层使用了操作系统mutex指令, 造成内核态和用户态的切换),这里我们通过原子整型来使用 无锁 思想来解决此问题

  1. class AccountCas implements Account {
  2. //使用原子整数类型: 底层使用CAS+重试的机制,而不是传统的Integer整数类型
  3. private AtomicInteger balance;
  4. public AccountCas(int balance) {
  5. this.balance = new AtomicInteger(balance);
  6. }
  7. @Override
  8. public Integer getBalance() {
  9. //得到原子整数的值
  10. return balance.get();
  11. }
  12. @Override
  13. public void withdraw(Integer amount) {
  14. while(true) {
  15. //获得修改前的最新值
  16. int prev = balance.get();
  17. //获得修改后的值
  18. int next = prev - amount;
  19. /*
  20. 此时的prev、next都是(局部变量)存储在工作内存中的,还没有同步到主内存中
  21. */
  22. //真正修改,第一个参数是调用get方法获得的值,第二个参数是要修改的值,修改成功会返回true
  23. if(balance.compareAndSet(prev, next)) {
  24. break;
  25. }
  26. }
  27. }
  28. }

底层原理概述—->CAS

上面使用AtomicInteger的案例解决方法,内部并没有用锁也能实现共享变量的线程安全。那么它是如何实现的呢?
image.png
其中的关键是 compareAndSwap方法(比较并设置值),它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。
我们来分析一下它具体是怎么实现的:
image.png
流程 :

  • 当一个线程要去修改Account对象中的值时,先调用get方法获取值prev(),然后再将其设置为新的值next(调用cas方法)。在调用cas方法时,会将prev与Account中的余额进行比较(满足一定条件才能更新成功)。
    • 如果两者相等,就说明该值还未被其他线程修改,此时便可以进行修改操作。
    • 如果两者不相等,就不设置值,重新获取值prev(调用get方法),然后再将其设置为新的值next(调用cas方法),直到修改成功为止。
  • 假如在执行cas方法之前,另外的一个线程就已经将其修改为90;你就得在其他线程修改的基础上再进行修改(人家修改为90,你就得在90的基础上进行你的增减操作)。既然其他线程修改了,所以你之前调用的get方法获取的prev跟共享变量上的最新结果不一样,所以本次修改失败,返回false;再次进行循环,如此反复直到修改成功。
  • cas方法是怎么知道共享变量被其他线程修改的?比较的结果不一样呗

注意 :

  • 其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。
  • 在多核状态下,某个核执行到带 lock 前缀的指令时,CPU 会让总线锁住,当当前这个核(线程)把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。

volatile 在 CAS中的作用

  • 在上面代码中的AtomicInteger类,保存值的value属性使用了volatile 修饰。获取共享变量时,为了保证该共享变量的可见性,需要使用 volatile 修饰。

image.png

  • volatile可以用来修饰 成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。这就是可见性的体现,即一个线程对 volatile 变量的修改,对另一个线程可见。
  • 注意: volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
  • **CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果**

    为什么CAS+重试(无锁)效率高

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

    • 打个比喻:线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行状态,代价比较大。
  • 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片**,仍然会进入可运行状态,还是会导致上下文切换。**

    CAS 的特点 (乐观锁和悲观锁的特点)

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

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。

  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
  • CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
    • 但如果竞争激烈(写操作多),可以想到重试必然频繁发生,反而效率会受影响

原子类

基本类

java.util.concurrent.atomic并发包提供了一些并发工具类:使用原子操作的方式 (共享数据为基本数据类型原子类)

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean :布尔型原子类

通过观察源码可以发现这几个类内部都是通过cas的原理来实现的,而cas底层又是使用到了,sun.misc.Unsafe类来实现;换句话说CAS的底层是sun.misc.Unsafe类,这里先不说这个类,后面会具体分析到它,先来看看这几个原子基本类;
这三个类提供的方法几乎相同,所以我们将以 AtomicInteger为例子来介绍。先讨论原子整数类,以 AtomicInteger 为例讨论它的api方法

AtomicInteger API演示

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

updateAndGet的实现

上面演示的方法都只能做一些简单的加法减法,这里我们演示一下稍微更复杂一点的操作,比如除法或者乘法;

简单写法:

  1. public static void main(String[] args) {
  2. //想要在5的基础上进行乘法运算
  3. AtomicInteger i = new AtomicInteger(5);
  4. //简单写法
  5. i.updateAndGet(value ->value * 10);
  6. System.out.println(i.get()); // 2
  7. }

想做更复杂的运算的话

步骤:

  • 调用updateAndGet方法, 将共享变量i,以及 IntUnaryOperator对象传递过去
  • updateAndGet方法内部, 传过来的operator对象, 调用IntUnaryOperator中的applyAsInt方法, 实际调用的就是传递过来的对象的方法, 进行 / 操作 ```java public static void main(String[] args) {
  1. AtomicInteger i = new AtomicInteger(5);
  2. updateAndGet(i, new IntUnaryOperator() {
  3. @Override
  4. public int applyAsInt(int operand) {
  5. return operand / 2;
  6. }
  7. });
  8. System.out.println(i.get()); // 2
  9. }
  10. /**
  11. * 如果让我们来实现线程安全计算的话该如何?
  12. * 这里我们考虑使用最基本的compareAndSet思想来实现;
  13. * 使用一个while(true)循环来实现,一旦有其他线程将其修改,就进行重试
  14. * 直到期待值跟最新值相等
  15. * @param i
  16. * @param operator
  17. */
  18. public static void updateAndGet(AtomicInteger i, IntUnaryOperator operator) {
  19. while (true) {
  20. //获取到当前值
  21. int prev = i.get(); // 5
  22. //进行计算,并将结果存放在next中
  23. int next = operator.applyAsInt(prev);
  24. //使用compareAndSet方法进行比较并更新
  25. if (i.compareAndSet(prev, next)) {
  26. //若果成功了就break
  27. break;
  28. }
  29. }
  30. }
  1. <a name="iJjVQ"></a>
  2. ### 原子引用 (AtomicReference)类
  3. 原子引用的作用: **保证引用类型的共享变量是线程安全的(确保这个原子引用没有引用过别人**
  4. - 为什么需要原子引用类型 ? (引用数据类型原子类)
  5. 基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用引用类型原子类。
  6. - AtomicReference:引用类型原子类
  7. - AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,**可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。**
  8. - AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,也可以**解决使用 CAS 进行原子更新时可能出现的 ABA 问题**。
  9. 例子 : 使用原子引用实现BigDecimal存取款的[线程安全](https://so.csdn.net/so/search?q=%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8&spm=1001.2101.3001.7020):
  10. ```java
  11. interface DecimalAccount {
  12. // 获取余额
  13. BigDecimal getBalance();
  14. // 取款
  15. void withdraw(BigDecimal amount);
  16. /**
  17. * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
  18. * 如果初始余额为 10000 那么正确的结果应当是 0
  19. */
  20. static void demo(DecimalAccount account) {
  21. List<Thread> ts = new ArrayList<>();
  22. for (int i = 0; i < 1000; i++) {
  23. ts.add(new Thread(() -> {
  24. account.withdraw(BigDecimal.TEN);
  25. }));
  26. }
  27. ts.forEach(Thread::start);
  28. ts.forEach(t -> {
  29. try {
  30. t.join();
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. });
  35. System.out.println(account.getBalance());
  36. }
  37. }

下面这个是不安全的实现过程:

  1. class DecimalAccountUnsafe implements DecimalAccount {
  2. BigDecimal balance;
  3. public DecimalAccountUnsafe(BigDecimal balance) {
  4. this.balance = balance;
  5. }
  6. @Override
  7. public BigDecimal getBalance() {
  8. return balance;
  9. }
  10. @Override
  11. public void withdraw(BigDecimal amount) {
  12. BigDecimal balance = this.getBalance();
  13. this.balance = balance.subtract(amount);
  14. }
  15. }

解决代码如下:在AtomicReference类中,存在一个value类型的变量,保存对BigDecimal对象的引用。

  1. public class Test1 {
  2. public static void main(String[] args) {
  3. DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000")));
  4. }
  5. }
  6. class DecimalAccountCas implements DecimalAccount {
  7. //原子引用,泛型类型为小数类型,保护BigDecimal
  8. private final AtomicReference<BigDecimal> balance;
  9. public DecimalAccountCas(BigDecimal balance) {
  10. //创建的是一个AtomicReference
  11. this.balance = new AtomicReference<>(balance);
  12. }
  13. @Override
  14. public BigDecimal getBalance() {
  15. return balance.get();
  16. }
  17. @Override
  18. public void withdraw(BigDecimal amount) {
  19. while (true) {
  20. BigDecimal prev = balance.get();
  21. BigDecimal next = prev.subtract(amount);
  22. if (balance.compareAndSet(prev, next)) {
  23. break;
  24. }
  25. }
  26. }
  27. }

ABA 问题及解决 (重点)

  • 如下程序所示,虽然 在other方法中存在两个线程对共享变量进行了修改,**但是修改之后又变成了原值main线程对`修改过共享变量的过程`是不可见的,这种操作这对业务代码并无影响。** ```java public class Test1 { //该变量的初始值是A,我们想在后面将其修改为C static AtomicReference ref = new AtomicReference<>(“A”);

    public static void main(String[] args) {

    1. new Thread(() -> {
    2. String pre = ref.get();
    3. System.out.println("change");
    4. try {
    5. other();
    6. } catch (InterruptedException e) {
    7. e.printStackTrace();
    8. }
    9. Sleeper.sleep(1);
    10. //把ref中的A改为C
    11. System.out.println("change A->C " + ref.compareAndSet(pre, "C"));
    12. }).start();

    }

    static void other() throws InterruptedException {

    1. new Thread(() -> {
    2. // 此时ref.get()为A,此时共享变量ref也是A,没有被改过, 此时CAS
    3. // 可以修改成功, B
    4. System.out.println("change A->B " + ref.compareAndSet(ref.get(), "B"));
    5. }).start();
    6. Thread.sleep(500);
    7. new Thread(() -> {
    8. // 同上, 修改为A
    9. System.out.println("change B->A " + ref.compareAndSet(ref.get(), "A"));
    10. }).start();

    } }

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/28814483/1652954967095-4a7020f3-1267-4865-a06c-c9f4dad111e8.png#clientId=u5326913a-fb1e-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=130&id=ue1b3ccd6&margin=%5Bobject%20Object%5D&name=image.png&originHeight=162&originWidth=716&originalType=binary&ratio=1&rotation=0&showTitle=false&size=22202&status=done&style=none&taskId=uaa44b1de-8638-4599-9138-2307620a997&title=&width=572.8)
  2. - 主线程仅能判断出共享变量的值与最初值 A是否相同,不能感知到这种从 A 改为 B 又改回 A 的情况,也就是说它不能判断出其他线程将A修改成另外的值然后又修改回A值的情况,在A变为B但没变回A之前,其他线程能做很多事情了!!!
  3. - 如果主线程希望:**只要有其它线程【修改过】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号。可以使用AtomicStampedReference来解决。**
  4. <a name="TrOYx"></a>
  5. #### AtomicStampedReference (加版本号解决ABA问题)
  6. - 谁修改过原来的值,就得将版本号进行更新,这样即可解决ABA问题
  7. ```java
  8. class Test1 {
  9. //指定版本号初始为0
  10. static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
  11. public static void main(String[] args) {
  12. new Thread(() -> {
  13. //获取的方法也有所变化
  14. String pre = ref.getReference();
  15. //获得版本号
  16. int stamp = ref.getStamp(); // 此时的版本号还是第一次获取的
  17. System.out.println("change");
  18. try {
  19. other();
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. try {
  24. Thread.sleep(1000);
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. //把ref中的A改为C,并比对版本号,如果版本号相同,就执行替换,并让版本号+1
  29. System.out.println("change A->C stamp " + stamp + ref.compareAndSet(pre, "C", stamp, stamp + 1));
  30. }).start();
  31. }
  32. static void other() throws InterruptedException {
  33. new Thread(() -> {
  34. int stamp = ref.getStamp();
  35. System.out.println("change A->B stamp " + stamp + ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1));
  36. }).start();
  37. Thread.sleep(500);
  38. new Thread(() -> {
  39. int stamp = ref.getStamp();
  40. System.out.println("change B->A stamp " + stamp + ref.compareAndSet(ref.getReference(), "A", stamp, stamp + 1));
  41. }).start();
  42. }
  43. }

image.png

AtomicMarkableReference (标记cas的共享变量是否被修改过)

  • AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A -> B -> A ->C,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。
  • 但是有时候,我们并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference ,用一个布尔值来记录是否被改动过

image.png

  1. @Slf4j(topic = "guizy.TestABAAtomicMarkableReference")
  2. class TestABAAtomicMarkableReference {
  3. public static void main(String[] args) throws InterruptedException {
  4. GarbageBag bag = new GarbageBag("装满了垃圾");
  5. // 参数2 mark 可以看作一个标记,true表示垃圾袋满了
  6. AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
  7. log.debug("主线程 start...");
  8. GarbageBag prev = ref.getReference();
  9. log.debug(prev.toString());
  10. new Thread(() -> {
  11. log.debug("打扫卫生的线程 start...");
  12. bag.setDesc("空垃圾袋");
  13. // 两个都是bag没有真正进行修改,只是将状态从true改成false
  14. while (!ref.compareAndSet(bag, bag, true, false)) {
  15. }
  16. log.debug(bag.toString());
  17. }).start();
  18. Thread.sleep(1000);
  19. log.debug("主线程想换一只新垃圾袋?");
  20. boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
  21. log.debug("换了么?" + success);
  22. log.debug(ref.getReference().toString());
  23. }
  24. }
  25. class GarbageBag {
  26. //对垃圾的描述
  27. String desc;
  28. public GarbageBag(String desc) {
  29. this.desc = desc;
  30. }
  31. public void setDesc(String desc) {
  32. this.desc = desc;
  33. }
  34. @Override
  35. public String toString() {
  36. return super.toString() + " " + desc;
  37. }
  38. }

image.png

AtomicStampedReference和AtomicMarkableReference两者的区别

  • AtomicStampedReference 需要我们传入 整型变量 作为版本号,来判定是否被更改过
  • AtomicMarkableReference需要我们传入布尔变量 作为标记,来判断是否被更改过

原子数组

  • 保证数组内的元素的线程安全
  • 使用原子的方式更新数组里的某个元素
    • AtomicIntegerArray:整形数组原子类
    • AtomicLongArray:长整形数组原子类
    • AtomicReferenceArray:引用类型数组原子类

上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍。实例代码

  • 普通数组内元素, 多线程访问会造成安全问题

    1. class AtomicArrayTest {
    2. public static void main(String[] args) {
    3. //想要获得的效果是在一个长度为10的数组里面,将其里面的每个元素从0自增到10000
    4. //调用demo方法
    5. demo(
    6. () -> new int[10],
    7. array -> array.length,
    8. (array, index) -> array[index]++,
    9. array -> System.out.println(Arrays.toString(array))
    10. );
    11. }
    12. /**
    13. * 参数1,提供数组、可以是线程不安全数组或线程安全数组
    14. * 参数2,获取数组长度的方法
    15. * 参数3,自增方法,回传 array, index
    16. * 参数4,打印数组的方法
    17. */
    18. // 函数式接口一:supplier 表示提供者 无中生有,没有参数,但是需要返回结果,如 ()->结果
    19. // 函数式接口二:function 表示函数, 一个参数一个返回结果,如 (参数)->结果 , 还有一种是两个参数返回一个结果,如BiFunction (参数1,参数2)->结果
    20. // 函数式接口三:consumer 表示消费者 一个参数但没结果 如 (参数)->void, 另一中两个参数,如BiConsumer (参数1,参数2)->void
    21. private static <T> void demo(Supplier<T> arraySupplier, Function<T, Integer> lengthFun,
    22. BiConsumer<T, Integer> putConsumer, Consumer<T> printConsumer) {
    23. List<Thread> ts = new ArrayList<>();
    24. T array = arraySupplier.get();
    25. int length = lengthFun.apply(array);
    26. for (int i = 0; i < length; i++) {
    27. // 创建10个线程, 每个线程对数组作 10000 次操作
    28. ts.add(new Thread(() -> {
    29. for (int j = 0; j < 10000; j++) {
    30. putConsumer.accept(array, j % length);
    31. }
    32. }));
    33. }
    34. ts.forEach(t -> t.start()); // 启动所有线程
    35. ts.forEach(t -> {
    36. try {
    37. t.join();
    38. } catch (InterruptedException e) {
    39. e.printStackTrace();
    40. }
    41. }); // 等所有线程结束
    42. printConsumer.accept(array);
    43. }
    44. }

    image.png

  • 使用AtomicIntegerArray来创建安全数组 ```java //方式一: demo(

    1. ()-> new AtomicIntegerArray(10),
    2. (array) -> array.length(),
    3. (array, index) -> array.getAndIncrement(index),
    4. array -> System.out.println(array)

    ); //方式二: demo(

    1. ()-> new AtomicIntegerArray(10),
    2. AtomicIntegerArray::length,
    3. AtomicIntegerArray::getAndIncrement,
    4. System.out::println

    );

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/28814483/1652959199247-b0b185b7-bfbc-40e3-8884-8d9837a853ed.png#clientId=u5326913a-fb1e-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=58&id=u141f25dc&margin=%5Bobject%20Object%5D&name=image.png&originHeight=72&originWidth=1034&originalType=binary&ratio=1&rotation=0&showTitle=false&size=27901&status=done&style=none&taskId=u005601b7-0c66-4701-8dd3-84b2d4fc0ba&title=&width=827.2)
  2. <a name="ooCU1"></a>
  3. ### 字段更新器
  4. 保证多线程访问同一个对象的成员变量时, **成员变量**的线程安全性。
  5. - AtomicReferenceFieldUpdater —引用类型的属性
  6. - AtomicIntegerFieldUpdater —整形的属性
  7. - AtomicLongFieldUpdater —长整形的属性
  8. 注意:利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/28814483/1652959234353-1670b38a-41eb-4d53-9973-6c4c9bcedd53.png#clientId=u5326913a-fb1e-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=47&id=u0f461b46&margin=%5Bobject%20Object%5D&name=image.png&originHeight=59&originWidth=1091&originalType=binary&ratio=1&rotation=0&showTitle=false&size=23391&status=done&style=none&taskId=u8f5fcc6f-2b89-409d-86d4-6eb8f0b7e01&title=&width=872.8)使用示例
  9. ```java
  10. public class AtomicFieldTest {
  11. public static void main(String[] args) {
  12. Student stu = new Student();
  13. // 获得原子更新器
  14. // 泛型
  15. // 参数1 持有属性的类 参数2 被更新的属性的类
  16. // newUpdater中的参数:第三个为属性的名称
  17. AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
  18. // 期望的为null, 如果name属性没有被别的线程更改过, 默认就为null, 此时匹配, 就可以设置name为张三
  19. System.out.println(updater.compareAndSet(stu, null, "张三"));
  20. System.out.println(updater.compareAndSet(stu, stu.name, "王五"));
  21. System.out.println(stu);
  22. }
  23. }
  24. class Student {
  25. volatile String name;
  26. @Override
  27. public String toString() {
  28. return "Student{" +
  29. "name='" + name + '\'' +
  30. '}';
  31. }
  32. }

image.png

原子累加器 LongAddr极其源码 (重要)

CAS底层原理

概述

java中提供了对CAS操作的支持,具体在sun.misc.unsafe类中。这个unsafe并不是指线程的不安全,而是因为它太底层了不建议程序员直接去操作它。

  • Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过**反射**获得(**该对象其源码被声明为单例且私有,故需要反射进行获取**)
  • 可以发现我们之前学习的CAS、LockSupport、AtomicInteger以及其他的原子类, 底层都使用的是**Unsafe**类中的相应方法

image.png

  • 其中底层的Unsafe有关CAS的相关方法实现原子操作的方法声明如下(当然该类的其他方法也可以操作内存) ```java /* 参数: var1:表示要操作的对象 var2:表示要操作的对象中属性地址的偏移量,我们只能知道对象的地址,对象中的属性地址得根据偏移量获取 var4:表示要修改的期望值,只有跟修改前一致才能进行修改 var5:表示要修改为的新值

这几个方法compareAndSwapXXXX,Object表示要修改对象的类型为Object,另外两个方法同理 */ public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

  1. 这里我们演示一下使用unsafe对象直接对Person对象中的属性进行线程安全的修改。因为它的底层是通过对内存的偏移量来定位到Person对象的属性的,所以比较复杂,在定位到属性后再进行比较操作;
  2. 所以我们首先要获取到属性的偏移量(偏移地址),这个可以调用unsafe对象的objectFieldOffset方法来获取;获取完偏移地址就可以对里面的属性值进行cas修改了。
  3. ```java
  4. public class Test {
  5. public static void main(String[] args) throws Exception {
  6. // 通过反射获得Unsafe对象
  7. Class unsafeClass = Unsafe.class;
  8. // 获得构造函数,Unsafe的构造函数为私有的
  9. Constructor constructor = unsafeClass.getDeclaredConstructor();
  10. // 设置为允许访问私有内容
  11. constructor.setAccessible(true);
  12. // 创建Unsafe对象
  13. Unsafe unsafe = (Unsafe) constructor.newInstance();
  14. // 创建Person对象
  15. Person person = new Person();
  16. // 获得其属性 name 的偏移量
  17. long nameOffset = unsafe.objectFieldOffset(Person.class.getDeclaredField("name"));
  18. long ageOffset = unsafe.objectFieldOffset(Person.class.getDeclaredField("age"));
  19. // 通过unsafe的CAS操作改变值
  20. //compareAndSwapObject方法是比较并交换引用地址
  21. unsafe.compareAndSwapObject(person, nameOffset, null, "张三");
  22. //compareAndSwapInt方法是比较并交换int
  23. unsafe.compareAndSwapInt(person, ageOffset, 0, 22);
  24. System.out.println(person);
  25. }
  26. }
  27. class Person {
  28. // 配合CAS操作,必须用volatile修饰
  29. volatile String name;
  30. volatile int age;
  31. @Override
  32. public String toString() {
  33. return "Person{" +
  34. "name='" + name + '\'' +
  35. ", age=" + age +
  36. '}';
  37. }
  38. }

使用Unsafe模拟实现原子类

设计的大体思路就是先利用反射获取到一个unsafe对象,并且计算出要保护的变量属性的偏移量;然后利用unsafe对象结合cas操作完成赋值时的原子性。

  1. class MyAtomicInteger{
  2. //MyAtomicInteger类中要保护的是int类型的value,因为要配合cas,所以用volatile进行修饰
  3. private volatile int value;
  4. private static final long valueOffset;
  5. //声明一个静态的unsafe对象
  6. private static final Unsafe UNSAFE;
  7. static {
  8. UNSAFE=UnsafeAccessor.getUnsafe();
  9. //计算要保护变量的偏移量
  10. try {
  11. valueOffset=UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
  12. } catch (NoSuchFieldException e) {
  13. e.printStackTrace();
  14. //要通过unsafe对象操作数据需要先得到域的偏移量,然后得到偏移量时可能抛出找不到域的异常,由于这种异常不会发生,
  15. // 所以转化成非受查异常。在后续调用方法就不用处理这个异常。
  16. throw new RuntimeException(e);
  17. }
  18. }
  19. public int getValue(){
  20. return value;
  21. }
  22. public void decrement(int amount){
  23. //while循环保证赋值时的原子性
  24. while (true){
  25. int prev = this.value;
  26. int next = prev-amount;
  27. //利用unsafe对象完成赋值
  28. if (UNSAFE.compareAndSwapInt(this,valueOffset,prev,next)){
  29. break;
  30. }
  31. }
  32. }
  33. }
  34. /*
  35. * 获取一个unsafe对象
  36. * */
  37. class UnsafeAccessor{
  38. private static final Unsafe UNSAFE;
  39. static {
  40. try {
  41. Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
  42. theUnsafe.setAccessible(true);
  43. UNSAFE= (Unsafe) theUnsafe.get(null);
  44. }catch (NoSuchFieldException | IllegalAccessException e){
  45. throw new Error(e);
  46. }
  47. }
  48. public static Unsafe getUnsafe(){
  49. return UNSAFE;
  50. }
  51. }

CAS的实现原理

原理概述

CAS操作是通过调用JNI(Java本地接口)的代码实现,JNI: java Native Interface,允许java调用其它的语言。而compareAndSwapxxx系列的方法就是借助“C语言”来调用cpu底层指令实现的。
以常用的Intel x86(英特尔)平台来说,最终映射到的cpu的指令为“cmpxchg”,这是一个原子指令,涉及汇编语言,给相应指令加了lock前缀进行lock加锁保证原子,cpu执行此命令时,实现比较并替换的操作!
也就是说在现代计算机动不动就上百核心的情况下,cmpxchg是这样保证多核心下的线程安全的:
在系统底层进行CAS操作的时候,会判断当前系统是否为多核心系统,如果是就给“总线”加锁,只有一个线程会对总线加锁成功,加锁成功之后才会执行CAS操作,也就是说CAS的原子性是平台级别的 !

CAS存在的ABA问题

在之前原子类那里已经介绍过了,这里不再赘述。