共享模型之内存

上一章讲解的 Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性.
这一章我们进一步深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题

Java 内存模型

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。

JMM 体现在以下几个方面

  1. 原子性 - 保证指令不会受到线程上下文切换的影响
  2. 可见性 - 保证指令不会受 cpu 缓存的影响
  3. 有序性 - 保证指令不会受 cpu 指令并行优化的影响

可见性

退不出的循环
先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

  1. static boolean run = true;
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread t = new Thread(()->{
  4. while(run){
  5. // ....
  6. }
  7. });
  8. t.start();
  9. sleep(1);
  10. run = false; // 线程t不会如预想的停下来
  11. }

为什么呢?分析一下:
1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存
image.png
2. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中, 减少对主存中 run 的访问,提高效率
image.png
3. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
image.png

解决方法
volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。

补充

synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized 是属于重量级操作,性能相对更低。
synchnorized可见性原因:
1)线程解锁前,必须把共享变量的最新值刷新到主内存中
2)线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值

如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到 对 run 变量的修改了,想一想为什么?

  1. public void println(String x) {
  2. synchronized (this) {
  3. print(x);
  4. newLine();
  5. }
  6. }

内存屏障 Memory Barrier(Memory Fence)

有序性

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

  1. static int i;
  2. static int j;
  3. // 在某个线程内执行如下赋值操作
  4. i = ...;
  5. j = ...;

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是

  1. i = ...;
  2. j = ...;

也可以是

  1. j = ...;
  2. i = ...;

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU执行指令的原理来理解一下吧

补充:

Clock Cycle Time
主频的概念大家接触的比较多,而 CPU 的 Clock Cycle Time(时钟周期时间),等于主频的倒数,意思是 CPU 能够识别的最小时间单位,比如说 4G 主频的 CPU 的 Clock Cycle Time 就是 0.25 ns,作为对比,我们墙上挂钟的Cycle Time 是 1s
例如,运行一条加法指令一般需要一个时钟周期时间

CPI
有的指令需要更多的时钟周期时间,所以引出了 CPI (Cycles Per Instruction)指令平均时钟周期数

IPC
IPC(Instruction Per Clock Cycle)即 CPI 的倒数,表示每个时钟周期能够运行的指令数

CPU 执行时间
程序的 CPU 执行时间,即我们前面提到的 user + system 时间,可以用下面的公式来表示

  1. 程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time

指令重排序优化

每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据 - 写回 ,这 5 个阶段。
image.png
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行。
但指令重排的前提是,重排指令不能影响结果,例如

  1. // 可以重排的例子
  2. int a = 10; // 指令1
  3. int b = 20; // 指令2
  4. System.out.println( a + b );
  5. // 不能重排的例子(有指令依赖的不可能重排序)
  6. int a = 10; // 指令1
  7. int b = a - 5; // 指令2

现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令的吞吐率。
image.png

诡异的结果

  1. public class test {
  2. int num = 0;
  3. boolean ready = false;
  4. // 线程1 执行此方法
  5. public void actor1(Result r) {
  6. if(ready) {
  7. r.r1 = num + num;
  8. } else {
  9. r.r1 = 1;
  10. }
  11. }
  12. // 线程2 执行此方法
  13. public void actor2(Result r) {
  14. num = 2;
  15. ready = true;
  16. }
  17. class Result{
  18. private int r1;
  19. }
  20. }

有同学这么分析
情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)

但结果还有可能是 0 !
这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2 。(指令交错且刚好重排序会导致指令交错执行的结果是错误的)
这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化。这个现象需要通过大量测试才能复现。

解决方法

volatile 修饰的变量,可以禁用指令重排。那么为什么它就可以禁用指令重排?并且为什么它具有可见性?

原理之 volatile

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

内存屏障两大功能

  • 可见性

Java通过几种原子操作完成工作内存和主内存的交互:

  1. 1. lock:作用于主内存,把变量标识为线程独占状态。
  2. 1. unlock:作用于主内存,解除独占状态。
  3. 1. read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
  4. 1. load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
  5. 1. use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
  6. 1. assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
  7. 1. store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
  8. 1. write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。

volatile的特殊规则就是:

  • read、load、use动作必须连续出现
  • assign、store、write动作必须连续出现

所以,使用volatile变量能够保证:

  • 每次读取前必须先从主内存刷新最新的值。
  • 每次写入后必须立即同步回主内存当中。

也就是说,volatile关键字修饰的变量看到的随时是自己的最新值。线程1中对变量v的最新修改,对线程2是可见的。

  • 有序性

在JVM中提供了四类内存屏障指令:
image.png

下面是基于保守策略的JMM内存屏障插入策略。

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的前面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障

那么该怎么理解这个插入策略?即在执行到内存屏障这句指令时,在它前面的操作已经全部完成后,后面的操作才可以进行。

但是要注意,volatile不能解决指令交错问题 !(也就是说不能解决多线程并发执行代码的顺序)

举个例子

  1. //x、y为非volatile变量
  2. //flag为volatile变量
  3. x = 2; //语句1
  4. y = 0; //语句2
  5. flag = true; //语句3
  6. x = 4; //语句4
  7. y = -1; //语句5
  1. public final class Singleton {
  2. private static volatile int a=0;
  3. public static void main(String[] args) {
  4. new Thread(()->{
  5. a=a+1;
  6. System.out.println(a+"-----------t1");
  7. },"t1").start();
  8. new Thread(()->{
  9. a=a-1;
  10. System.out.println(a+"-----------t2");
  11. },"t2").start();
  12. }
  13. }
  14. ——————————————————————————————————————————————————————
  15. 0-----------t1
  16. 0-----------t2


  • 由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
  • 并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

补充:

  1. 重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。synchronized和Lock保证每个时刻是有一个线程执行同步代码,虽然synchronized锁住的代码块有可能有指令重排序,但由于是单线程执行,所以对执行结果没影响,并且单线程是原子性的。
  2. 多线程的情况下,指令发生重排序可能会影响执行结果,volatile能保证它修饰的变量进行操作时有序,但多线程的指令交错是无法解决的(原子性),可以这么理解,volatile是为了减少多线程情况下指令执行发生错误的几率。

double-checked locking 问题

  1. public final class Singleton {
  2. private Singleton() { }
  3. private static Singleton INSTANCE = null;
  4. public static Singleton getInstance() {
  5. if(INSTANCE == null) { // t2
  6. // 首次访问会同步,而之后的使用没有 synchronized
  7. synchronized(Singleton.class) {
  8. if (INSTANCE == null) { // t1
  9. INSTANCE = new Singleton();
  10. }
  11. }
  12. }
  13. return INSTANCE;
  14. }
  15. }

对应的字节码

  1. 0 getstatic #2 <Singleton.INSTANCE>
  2. 3 ifnonnull 37 (+34)
  3. 6 ldc #3 <Singleton>
  4. 8 dup
  5. 9 astore_0
  6. 10 monitorenter
  7. 11 getstatic #2 <Singleton.INSTANCE>
  8. 14 ifnonnull 27 (+13)
  9. 17 new #3 <Singleton>
  10. 20 dup
  11. 21 invokespecial #4 <Singleton.<init>>
  12. 24 putstatic #2 <Singleton.INSTANCE>
  13. 27 aload_0
  14. 28 monitorexit
  15. 29 goto 37 (+8)
  16. 32 astore_1
  17. 33 aload_0
  18. 34 monitorexit
  19. 35 aload_1
  20. 36 athrow
  21. 37 getstatic #2 <Singleton.INSTANCE>
  22. 40 areturn

关键在 24行 将 Singleton实例 赋给 static INSTANCE这个静态变量;21行 调用Singleton这个类的构造方法。
有一个严重的问题,第一个 if 使用了 INSTANCE 变量,是在同步块之外,说明这个 INSTANCE 变量是不受synchronized控制,再加上synchronized里面的代码执行指令可能发生重排序,比如先执行24行,再执行21行,那么多线程情况下INSTANCE 变量就获得了没有执行过构造方法的一个不完整的实例。
image.png

解决方式就是给INSTANCE 变量加 volatile修饰,加入内存屏障保证 执行该变量的操作不会发生重排序。

happens-before

happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。

如何判断是否为 happens-before?

  • 程序次序规则: 在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作
    同一个线程中前面的所有写操作对后面的操作可见

  • 管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序)对同一个锁的lock操作。
    如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)

  • volatile变量规则:对一个volatile变量的写操作happen—before后面(时间上)对该变量的读操作。
    如果线程1写入了volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程)

  • 线程启动规则:Thread.start()方法happen—before调用用start的线程前的每一个操作。
    假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行前对线程B可见。注意:线程B启动之后,线程A在对变量修改线程B未必可见。

  • 线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
    (线程t1写入的所有变量,在任意其它线程t2调用t1.join(),或者t1.isAlive() 成功返回后,都对t2可见。)

  • 线程中断规则:对线程interrupt()的调用 happen—before 发生于被中断线程的代码检测到中断时事件的发生。
    (线程t1写入的所有变量,调用Thread.interrupt(),被打断的线程t2,可以看到t1的全部操作)

  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
    (对象调用finalize()方法时,对象初始化完成的任意操作,同步到全部主存同步到全部cache。)

  • 传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。
    A h-b B , B h-b C 那么可以得到 A h-b C

在程序运行过程中,所有的变更会先在寄存器或本地cache中完成,然后才会被拷贝到主存以跨越内存栅栏(本地或工作内存到主存之间的拷贝动作),此种跨越序列或顺序称为happens-before。happens-before本质是顺序,重点是跨越内存栅栏。通常情况下,写操作必须要happens-before读操作,即写线程需要在所有读线程跨越内存栅栏之前完成自己的跨越动作,其所做的变更才能对其他线程可见。


共享模型之无锁

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

解决思路-锁

  1. public class Test1 {
  2. public static void main(String[] args) {
  3. Account.demo(new AccountUnsafe(10000));
  4. Account.demo(new AccountCas(10000));
  5. }
  6. }
  7. class AccountUnsafe implements Account {
  8. private Integer balance;
  9. public AccountUnsafe(Integer balance) {
  10. this.balance = balance;
  11. }
  12. @Override
  13. public Integer getBalance() {
  14. synchronized (this) {
  15. return balance;
  16. }
  17. }
  18. @Override
  19. public void withdraw(Integer amount) {
  20. // 通过这里加锁就可以实现线程安全,不加就会导致线程安全问题
  21. synchronized (this) {
  22. balance -= amount;
  23. }
  24. }
  25. }
  26. interface Account {
  27. // 获取余额
  28. Integer getBalance();
  29. // 取款
  30. void withdraw(Integer amount);
  31. /**
  32. * Java8之后接口新特性, 可以添加默认方法
  33. * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
  34. * 如果初始余额为 10000 那么正确的结果应当是 0
  35. */
  36. static void demo(Account account) {
  37. List<Thread> ts = new ArrayList<>();
  38. long start = System.nanoTime();
  39. for (int i = 0; i < 1000; i++) {
  40. ts.add(new Thread(() -> {
  41. account.withdraw(10);
  42. }));
  43. }
  44. ts.forEach(thread -> thread.start());
  45. ts.forEach(t -> {
  46. try {
  47. t.join();
  48. } catch (InterruptedException e) {
  49. e.printStackTrace();
  50. }
  51. });
  52. long end = System.nanoTime();
  53. System.out.println(account.getBalance()
  54. + " cost: " + (end - start) / 1000_000 + " ms");
  55. }
  56. }

解决思路-无锁

  1. class AccountCas implements Account {
  2. //使用原子整数: 底层使用CAS+重试的机制
  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. /*
  21. 此时的prev为共享变量的值, 如果prev被别的线程改了.也就是说: 自己读到的共享变量的值 和 共享变量最新值 不匹配,
  22. 就继续where(true),如果匹配上了, 将next值设置给共享变量.
  23. AtomicInteger中value属性, 被volatile修饰, 就是为了确保线程之间共享变量的可见性.
  24. */
  25. if(balance.compareAndSet(prev, next)) {
  26. break;
  27. }
  28. }
  29. }
  30. }

CAS 与 volatile (重点)

cas + 重试 的原理

前面看到的AtomicInteger的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?

  1. @Override
  2. public void withdraw(Integer amount) {
  3. // 核心代码
  4. // 需要不断尝试,直到成功为止
  5. while (true){
  6. // 比如拿到了旧值 1000
  7. int prev = balance.get();
  8. // 在这个基础上 1000-10 = 990
  9. int next = prev - amount;
  10. /*
  11. compareAndSet 保证操作共享变量安全性的操作:
  12. ① 线程A首先获取balance.get(),拿到当前的balance值prev
  13. ② 根据这个prev值 - amount值 = 修改后的值next
  14. ③ 调用compareAndSet方法, 首先会判断当初拿到的prev值,是否和现在的
  15. balance值相同;
  16. 3.1、如果相同,表示其他线程没有修改balance的值, 此时就可以将next值
  17. 设置给balance属性
  18. 3.2、如果不相同,表示其他线程也修改了balance值, 此时就设置next值失败,
  19. 然后一直重试, 重新获取balance.get()的值,计算出next值,
  20. 并判断本次的prev和balnce的值是否相同...重复上面操作
  21. */
  22. if (atomicInteger.compareAndSet(prev,next)){
  23. break;
  24. }
  25. }
  26. }

其中的关键是 compareAndSwap(比较并设置值),它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作
image.png

流程 :

当一个线程要去修改Account对象中的值时,先获取值prev(调用get方法),然后再将其设置为新的值next(调用cas方法)。在调用cas方法时,会将prev与Account中的余额进行比较。

  • 如果两者相等,就说明该值还未被其他线程修改,此时便可以进行修改操作。
  • 如果两者不相等,就不设置值,重新获取值prev(调用get方法),然后再将其设置为新的值next(调用cas方法),直到修改成功为止。

[

](https://blog.csdn.net/m0_37989980/article/details/111657782)

怎么保证CAS的操作是原子性的?

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

[

](https://blog.csdn.net/m0_37989980/article/details/111657782)

volatile的作用

在上面代码中的AtomicInteger类,保存值的value属性使用了volatile 修饰。获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。
volatile可以用来修饰 成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。

再次注意: volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
[

](https://blog.csdn.net/m0_37989980/article/details/111657782)

为什么无锁效率高

使用CAS+重试。无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
打个比喻:线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。——线程数小于等于CPU核心数CAS才有优势,否则还是会让多出来的线程没时间片分给它而等待时间片,也会导致上下文切换开销。
[

](https://blog.csdn.net/m0_37989980/article/details/111657782)

CAS 的特点(和synchronized锁比较)

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

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一,但如果竞争激烈(写操作多),可以想到重试必然频繁发生,反而效率会受影响

原子整数

java.util.concurrent.atomic并发包提供了一些并发工具类,这里把它分成五类:

使用原子的方式 (共享数据为基本数据类型原子类)

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

上面三个类提供的方法几乎相同,所以我们将以 AtomicInteger为例子来介绍。

先讨论原子整数类,以 AtomicInteger 为例讨论它的api接口:通过观察源码可以发现
AtomicInteger 内部都是通过cas的原理来实现的

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

举个例子: updateAndGet的实现

  1. //源码
  2. public final int updateAndGet(IntUnaryOperator updateFunction) {
  3. int prev, next;
  4. do {
  5. prev = get();
  6. next = updateFunction.applyAsInt(prev);
  7. } while (!compareAndSet(prev, next));
  8. return next;
  9. }
  10. //应用
  11. public static void main(String[] args) {
  12. AtomicInteger integer=new AtomicInteger(5);
  13. integer.updateAndGet(new IntUnaryOperator() {
  14. @Override
  15. public int applyAsInt(int operand) {
  16. return operand / 2;
  17. }
  18. });
  19. System.out.println(integer.get());
  20. }

原子引用

为什么需要原子引用类型?
保证引用类型的共享变量是线程安全的(确保这个原子引用没有引用过别人)

  • AtomicReference
  • AtomicStampedReference (可以解决ABA问题)
  • AtomicMarkableReference

基本类型原子类只能更新一个变量,如果需要原子更新多个变量,或者说操作的是对象,那么需要使用引用类型原子类。

例子 : 使用原子引用实现BigDecimal存取款的线程安全:
下面这个是不安全的实现过程:

  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. }

安全实现—使用CAS锁

  1. class DecimalAccountCas implements DecimalAccount {
  2. //原子引用,泛型类型为小数类型
  3. private final AtomicReference<BigDecimal> balance;
  4. public DecimalAccountCas(BigDecimal balance) {
  5. this.balance = new AtomicReference<>(balance);
  6. }
  7. @Override
  8. public BigDecimal getBalance() {
  9. return balance.get();
  10. }
  11. @Override
  12. public void withdraw(BigDecimal amount) {
  13. while (true) {
  14. BigDecimal prev = balance.get();
  15. BigDecimal next = prev.subtract(amount);
  16. if (balance.compareAndSet(prev, next)) {
  17. break;
  18. }
  19. }
  20. }
  21. }

ABA 问题及解决

如下程序所示,虽然 在other方法中存在两个线程对共享变量进行了修改,但是修改之后又变成了原值,main线程对修改过共享变量的过程是不可见的,这种操作这对业务代码并无影响。

  1. public class Test1 {
  2. static AtomicReference<String> ref = new AtomicReference<>("A");
  3. public static void main(String[] args) {
  4. new Thread(() -> {
  5. String pre = ref.get();
  6. System.out.println("change");
  7. try {
  8. other();
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. Sleeper.sleep(1);
  13. //把ref中的A改为C
  14. System.out.println("change A->C " + ref.compareAndSet(pre, "C"));
  15. }).start();
  16. }
  17. static void other() throws InterruptedException {
  18. new Thread(() -> {
  19. // 此时ref.get()为A,此时共享变量ref也是A,没有被改过, 此时CAS
  20. // 可以修改成功, B
  21. System.out.println("change A->B " + ref.compareAndSet(ref.get(), "B"));
  22. }).start();
  23. Thread.sleep(500);
  24. new Thread(() -> {
  25. // 同上, 修改为A
  26. System.out.println("change B->A " + ref.compareAndSet(ref.get(), "A"));
  27. }).start();
  28. }
  29. }

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

AtomicStampedReference (加版本号解决ABA问题)

  1. public class Test1 {
  2. //指定版本号
  3. static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
  4. public static void main(String[] args) {
  5. new Thread(() -> {
  6. String pre = ref.getReference();
  7. //获得版本号
  8. int stamp = ref.getStamp(); // 此时的版本号还是第一次获取的
  9. System.out.println("change");
  10. try {
  11. other();
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. try {
  16. Thread.sleep(1000);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. //把ref中的A改为C,并比对版本号,如果版本号相同,就执行替换,并让版本号+1
  21. System.out.println("change A->C stamp " + stamp + ref.compareAndSet(pre, "C", stamp, stamp + 1));
  22. }).start();
  23. }
  24. static void other() throws InterruptedException {
  25. new Thread(() -> {
  26. int stamp = ref.getStamp();
  27. System.out.println("change A->B stamp " + stamp + ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1));
  28. }).start();
  29. Thread.sleep(500);
  30. new Thread(() -> {
  31. int stamp = ref.getStamp();
  32. System.out.println("change B->A stamp " + stamp + ref.compareAndSet(ref.getReference(), "A", stamp, stamp + 1));
  33. }).start();
  34. }
  35. }

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

  1. public class TestABAAtomicMarkableReference {
  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. new Thread(() -> {
  10. log.debug("打扫卫生的线程 start...");
  11. bag.setDesc("空垃圾袋");
  12. while (!ref.compareAndSet(bag, bag, true, false)) {
  13. }
  14. log.debug(bag.toString());
  15. }).start();
  16. Thread.sleep(1000);
  17. log.debug("主线程想换一只新垃圾袋?");
  18. //这里用prev是因为原子引用类型引用的对象多线程可见
  19. boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
  20. log.debug("换了么?" + success);
  21. log.debug(ref.getReference().toString());
  22. }
  23. }
  24. class GarbageBag {
  25. String desc;
  26. public GarbageBag(String desc) {
  27. this.desc = desc;
  28. }
  29. public void setDesc(String desc) {
  30. this.desc = desc;
  31. }
  32. @Override
  33. public String toString() {
  34. return super.toString() + " " + desc;
  35. }
  36. }
  1. 23:00:24.062 guizy.TestABAAtomicMarkableReference [main] - 主线程 start...
  2. 23:00:24.069 guizy.TestABAAtomicMarkableReference [main] - com.guizy.cas.GarbageBag@2be94b0f 装满了垃圾
  3. 23:00:24.312 guizy.TestABAAtomicMarkableReference [Thread-0] - 打扫卫生的线程 start...
  4. 23:00:24.313 guizy.TestABAAtomicMarkableReference [Thread-0] - com.guizy.cas.GarbageBag@2be94b0f 空垃圾袋
  5. 23:00:25.313 guizy.TestABAAtomicMarkableReference [main] - 主线程想换一只新垃圾袋?
  6. 23:00:25.314 guizy.TestABAAtomicMarkableReference [main] - 换了么?false
  7. 23:00:25.314 guizy.TestABAAtomicMarkableReference [main] - com.guizy.cas.GarbageBag@2be94b0f 空垃圾袋

AtomicStampedReference和AtomicMarkableReference两者的区别

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

原子数组 (AtomicIntegerArray)

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

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

  1. public class 原子数组 {
  2. public static void main(String[] args) {
  3. demo(
  4. // ()->new int[10],
  5. // (array)->array.length,
  6. // (array,index)->array[index]++,
  7. // array-> System.out.println(Arrays.toString(array))
  8. ()->new AtomicIntegerArray(10),
  9. (array)->array.length(),
  10. (array,index)->array.getAndIncrement(index),
  11. (array)-> System.out.println(array)
  12. );
  13. }
  14. 参数1,提供数组、可以是线程不安全数组或线程安全数组
  15. 参数2,获取数组长度的方法
  16. 参数3,自增方法,回传 array, index
  17. 参数4,打印数组的方法
  18. // supplier 提供者 无中生有 ()->结果
  19. // function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
  20. // consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
  21. private static <T> void demo(
  22. Supplier<T> arraySupplier,
  23. Function<T,Integer> lengthFun,//有参有返回结果
  24. BiConsumer<T,Integer> putConsumer,
  25. Consumer<T> printConsumer
  26. )
  27. {
  28. List<Thread> ts = new ArrayList<>();
  29. T array = arraySupplier.get();
  30. Integer length = lengthFun.apply(array);
  31. for (int i = 0; i < length; i++) {
  32. ts.add(new Thread(()->{
  33. for (int j = 0; j < 10000; j++) {
  34. putConsumer.accept(array,j%length);
  35. }
  36. }));
  37. }
  38. ts.forEach(t->t.start());
  39. ts.forEach((t)->{
  40. try {
  41. t.join();
  42. } catch (InterruptedException e) {
  43. e.printStackTrace();
  44. }
  45. });
  46. printConsumer.accept(array);
  47. }
  48. }

字段更新器

保证多线程访问同一个对象的成员变量时, 成员变量的线程安全性。

  • AtomicReferenceFieldUpdater ——-引用类型的属性
  • AtomicIntegerFieldUpdater ——-整形的属性
  • AtomicLongFieldUpdater ——-长整形的属性

注意:利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常。

  1. Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
  1. @Slf4j(topic = "guizy.AtomicFieldTest")
  2. public class AtomicFieldTest {
  3. public static void main(String[] args) {
  4. Student stu = new Student();
  5. // 获得原子更新器
  6. // 泛型
  7. // 参数1 持有属性的类 参数2 被更新的属性的类
  8. // newUpdater中的参数:第三个为属性的名称
  9. AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
  10. // 期望的为null, 如果name属性没有被别的线程更改过, 默认就为null, 此时匹配, 就可以设置name为张三
  11. System.out.println(updater.compareAndSet(stu, null, "张三"));
  12. System.out.println(updater.compareAndSet(stu, stu.name, "王五"));
  13. System.out.println(stu);
  14. }
  15. }
  16. class Student {
  17. volatile String name;
  18. @Override
  19. public String toString() {
  20. return "Student{" +
  21. "name='" + name + '\'' +
  22. '}';
  23. }
  24. }

原子累加器 (LongAddr) (重要)

  • LongAddr
  • LongAccumulator
  • DoubleAddr
  • DoubleAccumulator

原子整数 AtomicLong 和原子累加器 LongAddr 在累加方面的性能比较

  1. @Slf4j(topic = "guizy.Test")
  2. public class Test {
  3. public static void main(String[] args) {
  4. System.out.println("----AtomicLong----");
  5. for (int i = 0; i < 5; i++) {
  6. demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
  7. }
  8. System.out.println("----LongAdder----");
  9. for (int i = 0; i < 5; i++) {
  10. demo(() -> new LongAdder(), adder -> adder.increment());
  11. }
  12. }
  13. //adderSupplier 无参有返回
  14. //Consumer 有参无返回
  15. private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
  16. T adder = adderSupplier.get();
  17. long start = System.nanoTime();
  18. List<Thread> ts = new ArrayList<>();
  19. // 4 个线程,每人累加 50 万
  20. for (int i = 0; i < 40; i++) {
  21. ts.add(new Thread(() -> {
  22. for (int j = 0; j < 500000; j++) {
  23. action.accept(adder);
  24. }
  25. }));
  26. }
  27. ts.forEach(t -> t.start());
  28. ts.forEach(t -> {
  29. try {
  30. t.join();
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. });
  35. long end = System.nanoTime();
  36. System.out.println(adder + " cost:" + (end - start) / 1000_000);
  37. }
  38. }

LongAddr 性能提升的原因很简单,就是在有竞争时,设置多个累加单元(但不会超过cpu的核心数),Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。

AtomicLong 都是在一个共享资源变量上进行竞争, while(true)循环进行CAS重试, 性能没有LongAdder高。
[

](https://blog.csdn.net/m0_37989980/article/details/111657782)

LongAdder原理 (了解)

源码之 LongAdder

  1. public class LongAdder extends Striped64 implements Serializable{...}

LongAdder 继承了Striped64,Striped64又是什么?

  • Striped64是一个高并发累加的工具类。
  • Striped64设计核心思路就是通过内部的分散计算来避免竞争。
  • Striped64内部包含一个base和一个Cell[] cells数组,又叫hash表。 没有竞争的情况下,要累加的数通过cas累加到base上;如果有竞争的话,会将要累加的数累加到Cells数组中的某个cell元素里面。

那么我们看看Striped64 核心属性

  1. // 存放cell的hash表,大小为2乘幂
  2. transient volatile Cell[] cells;
  3. // 基础值(1.无竞争时更新,2.cells数组初始化过程不可用时,也会通过cas累加到base)
  4. transient volatile long base;
  5. // 自旋锁,通过CAS操作加锁 0无锁 1获得锁(初始化cells数组,创建cell单元,cells扩容)
  6. transient volatile int cellsBusy;

看看Striped64 中Cell类的源码

  1. @sun.misc.Contended static final class Cell {
  2. volatile long value;
  3. Cell(long x) { value = x; }
  4. //最重要的方法, 采取 cas 方式进行累加
  5. final boolean cas(long cmp, long val) {
  6. return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
  7. }
  8. ....

@sun.misc.Contended 是 Java 8 新增的一个注解,对某字段加上该注解则表示该字段会单独占用一个缓存行(Cache Line)。

为什么要添加@sun.misc.Contended注解?这就涉及到缓存行伪共享的原理了

原理之缓存行伪共享

缓存行伪共享得从缓存说起。

缓存与内存的速度比较
image.png
image.png

因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)。缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
CPU 要保证数据的一致性 (缓存一致性),如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
image.png
因为 Cell 是数组形式,在内存中是连续存储的,又因为缓存行大小够放两个Cell(一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value)),所以导致这两个Cell[0和1]都在一个缓存行。

这样问题来了:
Core-0 要修改 Cell[0]
Core-1 要修改 Cell[1]
无论谁修改成功,都会导致对方 Core 的缓存行失效。
@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,从而让 CPU 将Cell对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效

image.png

LongAdder的add方法源码

  1. public void add(long x) {
  2. // as 为cell数组的引用
  3. // b 为基础值
  4. // x 为累加值
  5. //uncontended=true代表当前线程对应的cell单元格CAS成功,false代表因出现竞争CAS失败
  6. Cell[] as; long b, v; int m; Cell a;
  7. //1.cells数组不为空,则进行下一个if判断
  8. //2.cells数组为空,则尝试进行cas base累加,失败则进行下一个if判断
  9. if ((as = cells) != null || !casBase(b = base, b + x)) {
  10. boolean uncontended = true;
  11. if (as == null || (m = as.length - 1) < 0 ||
  12. (a = as[getProbe() & m]) == null ||
  13. !(uncontended = a.cas(v = a.value, v + x)))
  14. /*
  15. 进入longAccumulate方法的三种状态
  16. 1.as == null || (m = as.length - 1) 当前cells数组为空,没有初始化 --->对应CASE-3
  17. 2.a = as[getProbe() & m]) == null 获取当前线程的hash值然后与数组长度进行&运算,获得对应cell单元格为空--->对应CASE-1
  18. 3.uncontended = a.cas(v = a.value, v + x) 前面判断得知当前对应的cell单元格存在,再次进行CAS,因竞争导致重试失败--->对应CASE-2
  19. */
  20. longAccumulate(x, null, uncontended);
  21. }
  22. }

image.png

longAccumulate()源码

  1. final void longAccumulate(long x, LongBinaryOperator fn,
  2. boolean wasUncontended) {
  3. int h;
  4. //当前线程还没有对应的cell,需要随机生成一个h值用来将当前线程绑定到cell
  5. if ((h = getProbe()) == 0) {
  6. //初始化probe
  7. ThreadLocalRandom.current();
  8. //h 对应新的probe值,用来对应cell
  9. h = getProbe();
  10. wasUncontended = true;
  11. }
  12. //collide 为true 表示需要扩容
  13. boolean collide = false;
  14. for (;;) {
  15. Cell[] as; Cell a; int n; long v;
  16. //CASE-1
  17. /*
  18. 二次判断 当前cells数组存在
  19. */
  20. if ((as = cells) != null && (n = as.length) > 0) {
  21. //二次判断 没有cell
  22. if ((a = as[(n - 1) & h]) == null) {
  23. //当前cellBusy为0 没有线程竞争
  24. if (cellsBusy == 0) {
  25. Cell r = new Cell(x);
  26. //获取当前cells数组的锁
  27. if (cellsBusy == 0 && casCellsBusy()) {
  28. //cell对象是否创建完成的标志位
  29. boolean created = false;
  30. try {
  31. /*
  32. 再次判断,老样子,防止系统调度原因出现线程的二次修改
  33. */
  34. Cell[] rs; int m, j;
  35. if ((rs = cells) != null &&
  36. (m = rs.length) > 0 &&
  37. rs[j = (m - 1) & h] == null) {
  38. rs[j] = r;
  39. // cell单元格创建完成,更新created为true
  40. created = true;
  41. }
  42. } finally {
  43. // 释放锁
  44. cellsBusy = 0;
  45. }
  46. //成功创建cell则break,失败则continue继续
  47. if (created)
  48. break;
  49. continue;
  50. }
  51. }
  52. collide = false;
  53. }
  54. //CASE-2:当前线程对应的cell单元格CAS写入数据失败出现竞争
  55. else if (!wasUncontended)
  56. wasUncontended = true;
  57. //CAS base尝试 成功跳出循环 失败进行下一次else-if
  58. else if (a.cas(v = a.value, ((fn == null) ? v + x :
  59. fn.applyAsLong(v, x))))
  60. break;
  61. //如果当前cell长度已经超过当前机器的CPU数量,拒绝扩容
  62. else if (n >= NCPU || cells != as)
  63. collide = false;
  64. else if (!collide)
  65. collide = true;
  66. //对cell数组进行扩容
  67. //获取到cells数组的锁,开始进行扩容
  68. else if (cellsBusy == 0 && casCellsBusy()) {
  69. try {
  70. //还是二次判断
  71. if (cells == as) {
  72. //cells数组扩容为原来2倍
  73. Cell[] rs = new Cell[n << 1];
  74. for (int i = 0; i < n; ++i)
  75. rs[i] = as[i];
  76. cells = rs;
  77. }
  78. } finally {
  79. //释放锁
  80. cellsBusy = 0;
  81. }
  82. collide = false;
  83. continue;
  84. }
  85. h = advanceProbe(h);
  86. }
  87. //CASE-3:上一步else-if失败,说明cells数组为空。对cell数组进行初始化
  88. else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
  89. /*
  90. cellBusy为0表示没有线程获取到当前cell的锁,通过CAS将cellBusy更新为1获取到锁,然后开始初始化cells数组
  91. init: cells数组是否初始化完成的标志位
  92. */
  93. boolean init = false;
  94. try {
  95. /*
  96. 再次判断当前cell数组是否为前面as引用。这里会出现一种情况,就是线程1执行到cellBusy=0后
  97. 因为系统调度让出CPU
  98. 这时线程2更新cellBusy成功并成功获取锁然后初始化cell数组成功并释放锁,最后将cellBusy置为0
  99. 然后线程1得到cpu时间继续更新cellBusy值获取锁成功,但此时cell数组已经被线程2初始化成功了,
  100. 此时线程1再次进行初始化会覆盖掉线程2已经初始化的cell数组。
  101. 声明:其他地方的判断都是防止系统调度原因防止线程再次操作而覆盖。
  102. */
  103. if (cells == as) {
  104. Cell[] rs = new Cell[2];
  105. //当前线程的hash值和1进行&运算,结果只会是0和1所以这个线程只会定位到cell[0]或者cell[1]的单元格
  106. rs[h & 1] = new Cell(x);
  107. cells = rs;
  108. // init为true表示cell数组初始化完成
  109. init = true;
  110. }
  111. } finally {
  112. // 更新cellBusy为0,释放锁
  113. cellsBusy = 0;
  114. }
  115. // cell数组初始化完成,跳出当前循环,开始下一轮循环
  116. if (init)
  117. break;
  118. }
  119. //CASE-4:上一步else-if失败,说明cellBusy设置为1失败并且获取cellBusy失败,说明出现了竞争
  120. //再次尝试CAS base(没用cell的那种)
  121. else if (casBase(v = base, ((fn == null) ? v + x :
  122. fn.applyAsLong(v, x))))
  123. break;
  124. }
  125. }

获取最终结果通过sum方法

  1. public long sum() {
  2. Cell[] as = cells; Cell a;
  3. long sum = base;
  4. if (as != null) {
  5. for (int i = 0; i < as.length; ++i) {
  6. if ((a = as[i]) != null)
  7. sum += a.value;
  8. }
  9. }
  10. return sum;
  11. }

遍历cell数组的value并和base进行求和,最终得到sum值。

Unsafe

  • Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得
  • 可以发现AtomicInteger以及其他的原子类, 底层都使用的是Unsafe类

使用底层的Unsafe类实现原子操作

  1. public class test {
  2. public static void main(String[] args) throws Exception{
  3. // 通过反射获得Unsafe对象
  4. Class<Unsafe> unsafeClass = Unsafe.class;
  5. // 获得构造函数,Unsafe的构造函数为私有的 如果是共有的不用Declared
  6. Constructor<Unsafe> constructor = unsafeClass.getDeclaredConstructor();
  7. // 设置为允许访问私有内容
  8. constructor.setAccessible(true);
  9. // 创建Unsafe对象实例
  10. Unsafe unsafe = constructor.newInstance();
  11. Person person = new Person();
  12. // 获得其属性 name 的偏移量
  13. long name = unsafe.objectFieldOffset(Person.class.getDeclaredField("name"));
  14. long age = unsafe.objectFieldOffset(Person.class.getDeclaredField("age"));
  15. // 通过unsafe的CAS操作改变值
  16. unsafe.compareAndSwapObject(person,name,null,"wzc");
  17. unsafe.compareAndSwapInt(person,age,0,22);
  18. System.out.println(person);
  19. unsafe.compareAndSwapObject(person,name,"wzc","wq");
  20. System.out.println(person);
  21. }
  22. }
  23. class Person {
  24. // 配合CAS操作,必须用volatile修饰
  25. volatile String name;
  26. volatile int age;
  27. @Override
  28. public String toString() {
  29. return "Person{" +
  30. "name='" + name + '\'' +
  31. ", age=" + age +
  32. '}';
  33. }
  34. }

共享模型之不可变

如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改。

这样的对象在 Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:

  1. public class test {
  2. public static void main(String[] args) throws Exception{
  3. DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
  4. for (int i = 0; i < 10; i++) {
  5. new Thread(()->{
  6. LocalDate date = dateTimeFormatter.parse("2022-10-01", LocalDate::from);
  7. System.out.println(date);
  8. }).start();
  9. }
  10. }
  11. }

不可变对象,实际是另一种避免竞争的方式。

不可变设计

final 的使用

另一个大家更为熟悉的 String 类也是不可变的,以它为例,说明一下不可变设计的要素

  1. public final class String
  2. implements java.io.Serializable, Comparable<String>, CharSequence {
  3. /** The value is used for character storage. */
  4. private final char value[];
  5. /** Cache the hash code for the string */
  6. private int hash; // Default to 0
  • final 修饰 String 类,保证子类不会破坏父类的方法
  • final 修饰 value[] 属性,保证只读,外界不可修改该属性
  • private 修饰 hash 属性,并且没有set方法,外界无法修改

保护性拷贝

但有同学会说,使用字符串时,也有一些跟修改相关的方法啊,比如 substring 等,那么下面就看一看这些方法是如何实现的,就以 substring 为例:

  1. public String substring(int beginIndex) {
  2. if (beginIndex < 0) {
  3. throw new StringIndexOutOfBoundsException(beginIndex);
  4. }
  5. int subLen = value.length - beginIndex;
  6. if (subLen < 0) {
  7. throw new StringIndexOutOfBoundsException(subLen);
  8. }
  9. return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
  10. }

发现其内部是调用 String 的构造方法创建了一个新字符串,再进入这个构造看看,是否对 fifinal char[] value 做出了修改:

  1. public String(char value[], int offset, int count) {
  2. if (offset < 0) {
  3. throw new StringIndexOutOfBoundsException(offset);
  4. }
  5. if (count <= 0) {
  6. if (count < 0) {
  7. throw new StringIndexOutOfBoundsException(count);
  8. }
  9. if (offset <= value.length) {
  10. this.value = "".value;
  11. return;
  12. }
  13. }
  14. // Note: offset or count might be near -1>>>1.
  15. if (offset > value.length - count) {
  16. throw new StringIndexOutOfBoundsException(offset + count);
  17. }
  18. this.value = Arrays.copyOfRange(value, offset, offset+count);
  19. }

结果也没有。是通过Arrays.copyOfRange() 方法 生成新的char[] value,然后对原有内容进行复制。
这种通过创建符本对象来避免共享的手段称之为 保护性拷贝

final 原理

  1. public class TestFinal {
  2. final int a = 20; }

字节码

  1. 0 aload_0
  2. 1 invokespecial #1 <java/lang/Object.<init>>
  3. 4 aload_0
  4. 5 bipush 20
  5. 7 putfield #2 <test.a>
  6. <-----------写屏障
  7. 10 return
  • 发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况。

注意:

  1. final成员变量表示常量 ,只能被赋值一次,赋值后值不再改变(final要求地址值不能改变)
  2. final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误
  3. 接口中声明的所有变量本身是final的(如果你在某接口的实现类A中把x改为其他值,那么另一个实现类B中对x有依赖的方法全部都出错了,这样接口还怎么能起到“模板”的作用呢)

享元设计模式

英文名称:Flyweight pattern。当需要重用数量有限的同一类对象时会用到享元模式。

体现

  • 包装类

在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包装类提供了 valueOf 方法,例如 Long 的 valueOf 会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对象

  1. public static Long valueOf(long l) {
  2. final int offset = 128;
  3. if (l >= -128 && l <= 127) { // will cache
  4. return LongCache.cache[(int)l + offset];
  5. }
  6. return new Long(l);
  7. }

注意:

  • Byte, Short, Long 缓存的范围都是 -128~127
  • Character 缓存的范围是 0~127
  • Integer的默认范围是 -128~127。最小值不能变 ,但最大值可以通过调整虚拟机参数 -Djava.lang.Integer.IntegerCache.high 来改变
  • Boolean 缓存了 TRUE 和 FALSE

Java中基本数据类型的装箱和拆箱操作

自动装箱

在JDK5以后,我们可以直接使用Integer num = 2;来进行值的定义,但是你有没有考虑过?Integer是一个对象呀,为什么我可以不实例化对象,就直接来进行Value的定义呢?

一般情况下我们在定义一个对象的时候,顶多赋值为一个null 即空值;
比如:Person pserson = null;但是肯定不可以Person person =2;这样操作吧,
那为什么Integer,Float,Double,等基本数据类型的包装类是可以直接定义值的呢?
究其原因无非是编译器在编译代码的时候,重新进行了一次实例化的操作而已啦:
比如当我们使用Integer num = 2 的时候,在JVM运行前的编译阶段,此时该Integer num = 2 将会被编译为
Integer num = new Integer(2); 那么此时编译后的这样一个语法 new Integer(2) 则是符合JDK运行时的规则的,而这种操作就是所谓的装箱操作;
注意:(不要拿Integer和int类型来进行对比,int,float,这些是JDK自定义的关键字,
本身在编译的时候就会被特殊处理,而Integer,Float,Double等则是标准的对象,对象的实现本身就是要有new 的操作才是合理;所以对于这些基本类型的包装类在进行 Integer num = 2的赋值时,则的确是必须要有一个装箱的操作将其变成对象实例化的方式这样也才是一个标准的过程;)

自动拆箱

那么当你了解了对应的装箱操作后,再来了解一下对应拆箱的操作:
当我们把一个原本的Integer num1 = 2; 来转换为 int num1 = 2的时候实际上就是一个拆箱的操作,及把包装类型转换为基本数据类型时便是所谓的拆箱操作;
一般当我们进行对比的时候,编译器便会优先把包装类进行自动拆箱:如Integer num1 = 2 和 int num2 = 2;当我们进行对比时 if(num1 == num2) 那么此时编译器便会自动的将包装类的num1自动拆箱为int类型进行对比等操作。

装箱及拆箱时的真正步骤

上述已经说过了自动装箱时,实际上是把 Integer num =2 编译时变更为了 Integer num = new Integer(2);
但实际上JDK真的就只是这么简单的进行了一下new的操作吗?当然不是,在自动装箱的过程中实际上是调用的Integer的valueOf(int i)的方法,来进行的装箱的操作;
我们来看一下这个方法的具体实现:我会直接在下述源码中加注释,直接看注释即可

  1. public static Integer valueOf(int i) {
  2. //在调用valueOf进行自动装箱时,会先进行一次所传入值的判断,当i的值大于等于IntegerCache.low 以及 小于等于IntegerCache.high时,则直接从已有的IntegerCache.cache中取出当前元素return即可;
  3. if (i >= IntegerCache.low && i <= IntegerCache.high){
  4. return IntegerCache.cache[i + (-IntegerCache.low)];
  5. }
  6. //否则则直接new Integer(i) 实例化一个新的Integer对象并return出去;
  7. return new Integer(i);
  8. }
  9. //此时我们再看一下上述的IntegerCache到底是做的什么操作,如下类:(注意:此处IntegerCache是 private 内部静态类,所以我们定义的外部类是无法直接使用的,此处看源码即可)
  10. private static class IntegerCache {
  11. //定义一个low最低值 及 -128;
  12. static final int low = -128;
  13. //定义一个最大值(最大值的初始化详情看static代码块)
  14. static final int high;
  15. //定义一个Integer数组,数组中存储的都是 new Integer()的数据;(数组的初始化详情看static代码块)
  16. static final Integer cache[];
  17. static {
  18. //此处定义一个默认的值为127;
  19. int h = 127;
  20. //sun.misc.VM.getSavedProperty() 表示从JVM参数中去读取这个"java.lang.Integer.IntegerCache.high"的配置,并赋值给integerCacheHighPropValue变量
  21. String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
  22. //当从JVM中所取出来的这个java.lang.Integer.IntegerCache.high值不为空时
  23. if (integerCacheHighPropValue != null) {
  24. try {
  25. //此处将JVM所读取出的integerCacheHighPropValue值进行parseInt的转换并赋值给 int i;
  26. int i = parseInt(integerCacheHighPropValue);
  27. //Math.max()方法含义是,当i值大于等于127时,则输出i值,否则则输出 127;并赋值给 i;
  28. i = Math.max(i, 127);
  29. //Math.min()则表示,当 i值 小于等于 Integer.MAX_VALUE时,则输出 i,否则输出 Integer.MAX_VALUE,并赋值给 h
  30. //此处使用:Integer.MAX_VALUE - (-low) -1 的原因是由于是从负数开始的,避免Integer最大值溢出,所以这样写的,此处可以先不考虑
  31. h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
  32. } catch( NumberFormatException nfe) {
  33. // If the property cannot be parsed into an int, ignore it.
  34. }
  35. }
  36. //最后把所得到的最终结果 h 赋值给我们亲爱的 high 属性;
  37. high = h;
  38. //以下赋值当前cache数组的最大长度;
  39. cache = new Integer[(high - low) + 1];
  40. int j = low;
  41. //然后进行cache数组的初始化循环;
  42. for(int k = 0; k < cache.length; k++)
  43. //注意:此处new Integer(j++);是先实例化的j,也就是负数-128,所以也才会有上述的Integer.MAX_VALUE - (-low) -1)的操作,因为数组中存储的是 -128 到 high 的所有实例化数据对象;
  44. cache[k] = new Integer(j++);
  45. // range [-128, 127] must be interned (JLS7 5.1.7)
  46. assert IntegerCache.high >= 127;
  47. }
  48. private IntegerCache() {}
  49. }

朋友们,由上述的代码我们便可以知道,自动装箱时:
1、high的值如果未通过JVM参数定义时则默认是127,当通过JVM参数进行定义后,则使用所定义的high值,前提是不超出(Integer.MAX_VALUE - (-low) -1)的长度即可,如果超出这个长度则默认便是:Integer.MAX_VALUE - (-low) -1;
2、默认情况下会存储一个 -128 到 high的 Integer cache[]数组,并且已经实例化了所有 -128 到high的Integer对象数据;
3、当使用valueOf(int i)来自动装箱时,会先判断一下当前所需装箱的值是否(大于等于IntegerCache.low && 小于等于IntegerCache.high) 如果是,则直接从当前已经全局初始化好的cache数组中返回即可,如果不是则重新 new Integer();
而当Integer对象在自动拆箱时则是调用的Integer的intValue()方法,方法代码如下:可以看出是直接把最初的int类型的value值直接返回了出去,并且此时返回的只是基本数据类型。

  1. private final int value;
  2. public int intValue() {
  3. return value;
  4. }

Integer于Int进行==比较时的代码案例

  1. public static void main(String[] args) {
  2. Integer num1 = 2000;
  3. int num2 = 2000;
  4. //会将Integer自动拆箱为int比较,此处为true;因为拆箱后便是 int于int比较,不涉及到内存比较的问题;
  5. System.out.println(num1 == num2);
  6. Integer num3 = new Integer(2000);
  7. Integer num4 = new Integer(2000);
  8. //此处为false,因为 num3 是实例化的一个新对象对应的是一个新的内存地址,而num4也是新的内存地址;
  9. System.out.println(num3 == num4);
  10. Integer num5 = 100;
  11. Integer num6 = 100;
  12. //返回为true,因为Integer num5 =100的定义方式,会被自动调用valueOf()进行装箱;而valueOf()装箱时是一个IntegerCache.high的判断的,只要在这个区间,则直接return的是数组中的元素
  13. //而num5 =100 及返回的是数组中下标为100的对象,而num6返回的也是数组中下标为 100的对象,所以两个对象是相同的对象,此时进行 == 比较时,内存地址相同,所以为true
  14. System.out.println(num5 == num6);
  15. Integer num7 = new Integer(100);
  16. Integer num8 = 100;
  17. //结果为false;为什么呢?因为num7并不是自动装箱的结果,而是自己实例化了一个新的对象,那么此时便是堆里面新的内存地址,而num8尽管是自动装箱,但返回的对象与num7的对象也不是一个内存地址哦;
  18. System.out.println(num7 == num8);
  19. }

总结(重点)

1、由于我们在使用Integer和int进行比较时,存在着自动拆箱于装箱的操作,所以在代码中进行Integer的对比时尽可能的使用 .equals()来进行对比;
比如我们定义如下一个方法:那么我们此时是无法知晓num1 和num2的值是否是直接new出来的?还是自动装箱定义出来的?就算两个值都是自动装箱定义出来的,那么num1 和num2的值是否超出了默认的-128到127的cache数组缓存呢?如果超出了那么还是new 的Integer(),此时我们进行 == 对比时,无疑是风险最大的,所以最好的还是 .equals()进行对比;除非是拿一个Integer和一个int基本类型进行对比可以使用,因为此时无论Integer是新new实例化的还是自动装箱的,在对比时都会被自动拆箱为 int基本数据类型进行对比;

2、合理的在项目上线后,使用-XX:AutoBoxCacheMax=20000 参数来定义自动装箱时的默认最大high值,可以很好的避免基本数据类型包装类被频繁堆内创建的问题;什么个意思呢,一般情况下我们在项目开发过程中,会大量使用Integer num = 23;等等的代码,并且我们在操作数据库的时候,一般返回的Entity实体类里面也会定义一大堆的Integer类型的属性,而上述也提到过了,每次Integer的使用实际上都会被自动装箱,对于超出-128和127的值,则会被创建新的堆对象;所以如果我们有很多的大于127的数据值,那么每次都需要在堆中创建临时对象岂不是一个很可惜的操作吗,如果我们在项目启动时设置-XX:AutoBoxCacheMax=20000,那么对于我们常用的Integer为2W以下的数字,则直接从IntegerCache 数组中直接取就行了,完全就没必要再创建临时的堆对象了嘛;这样对于整个JVM的GC回收来说,多多少少也是一些易处呀,避免了大量的重复的Integer对象的创建占用和回收的问题呢;不是嘛

3、首先我们看了上述自动装箱的源码以后,可以知道,初始化的缓存数据是定义在静态属性中的:static final Integer cache[]; 所以,答案是:我们自动装箱的cache数组缓存的确是定义在常量池中的;每次我们自动装箱时的数组判断,的确是从常量池中拿的数据,废话,因为是 static final 类型的呀,所以当然是常量池中存储的cache数组啦

  • 而我们在方法中定义局部变量 Integer a=300时,这个肯定是在堆或者常量池中啦(看是否自动装箱后使用常量池中cache);
  • 而对于我们在类中定义的成员属性来说,比如:static int a =3;此时则是在常量池中(无外乎什么类型因为他是静态的,所以常量池)而类的成员变量 int a=3,类的成员变量都在堆上

image.png