4.1 CPU物理缓存结构

image.png

4.2 并发编程的三大问题

并发编程的三大问题:原子性问题、可见性问题和有序性问题。

4.2.1 原子性问题

就是“不可中断的一个或一系列操作”,是指不会被线程调度机制打断的操作。这种操作一旦开始,就一直运行到结束,中间不会有任何线程的切换。

  1. class CounterSample
  2. {
  3. int sum = 0;
  4. public void increase() {
  5. sum++; //①
  6. }
  7. }

4.2.2 可见性问题

一个线程对共享变量的修改,另一个线程能够立刻可见,我们称该共享变量具备内存可见性。
image.png
要想解决多线程的内存可见性问题,所有线程都必须将共享变量刷新到主存,一种简单的方案是:使用Java提供的关键字volatile修饰共享变量。
所有的Object实例、Class实例和数组元素都存储在JVM堆内存中,堆内存在线程之间共享,所以存在可见性问题。

4.2.3 有序性问题

所谓程序的有序性,是指程序按照代码的先后顺序执行。如果程序执行的顺序与代码的先后顺序不同,并导致了错误的结果,即发生了有序性问题。

  1. package com.crazymakercircle.visiable;
  2. // 省略import
  3. public class InstructionReorder {
  4. private volatile static int x = 0, y = 0;
  5. private static int a = 0, b = 0;
  6. public static void main(String[] args) throws InterruptedException {
  7. int i = 0;
  8. for (;;) {
  9. i++;
  10. x = 0;
  11. y = 0;
  12. a = 0;
  13. b = 0;
  14. Thread one = new Thread(new Runnable() {
  15. public void run() {
  16. a = 1; //①
  17. x = b; //②
  18. }
  19. });
  20. Thread other = new Thread(new Runnable() {
  21. public void run() {
  22. b = 1; //③
  23. y = a; //④
  24. }
  25. });
  26. one.start();
  27. other.start();
  28. one.join();
  29. other.join();
  30. String result = "第" + i + "次 (" + x + "," + y + ")";
  31. if (x == 0 && y == 0) {
  32. System.err.println(result);
  33. }
  34. }
  35. }
  36. }

4.3 硬件层的MESI协议原理

4.3.1 总线锁和缓存锁

为了解决内存的可见性问题,CPU主要提供了两种解决办法:总线锁和缓存锁。

  1. 总线锁

image.png

  1. 缓存锁

image.png

4.3.2 MSI协议

4.3.3 MESI协议及RFO请求

  1. 初始阶段:开始时,缓存行没有加载任何数据,所以它处于“I状态”

image.png

  1. 本地写(Local Write)阶段:如果CPU内核写数据到处于“I状态”的缓存行,缓存行的状态就变成“M状态”。

image.png

4.3.4 volatile的原理

在正常情况下,系统操作并不会校验共享变量的缓存一致性,只有当共享变量用volatile关键字修饰了,该变量所在的缓存行才被要求进行缓存一致性的校验。
从volatile关键字的汇编代码出发分析一下volatile关键字的底层原理,参考如下示例代码:

  1. package com.crazymakercircle.visiable;
  2. public class VolatileVar
  3. {
  4. //使用volatile保障内存可见性
  5. volatile int var = 0;
  6. public void setVar(int var)
  7. {
  8. System.out.println("setVar = " + var);
  9. this.var = var;
  10. }
  11. public static void main(String[] args)
  12. {
  13. VolatileVar var = new VolatileVar();
  14. var.setVar(100);
  15. }
  16. }

4.4 有序性与内存屏障

在编译器和CPU都进行指令的重排优化时,可以通过在指令间插入一个内存屏障指令,告诉编译器和CPU,禁止在内存屏障指令前(或后)执行指令重排序。

4.4.2 As-if-Serial规则

  1. public class ReorderDemo{
  2. public static void main(String[] args) {
  3. int a=1; //①
  4. int b=2; //②
  5. int c=a+b; //③
  6. }
  7. }

为了保证As-if-Serial规则,Java异常处理机制也会为指令重排序做一些特殊处理。下面是一段非常简单的Java异常处理示例代码:

  1. public class ReorderDemo2{
  2. public static void main(String[] args) {
  3. int x, y;
  4. x = 1;
  5. try {
  6. x = 2; //①
  7. y = 0/0; //②
  8. } catch (Exception e) { //③
  9. } finally {
  10. System.out.println("x = " + x);
  11. }
  12. }
  13. }

As-if-Serial规则只能保障单内核指令重排序之后的执行结果正确,不能保障多内核以及跨CPU指令重排序之后的执行结果正确。

4.4.3 硬件层面的内存屏障

  1. 硬件层的内存屏障定义
  2. 硬件层的内存屏障的作用
  3. 内存屏障的使用示例

下面是一段可能乱序执行的代码:

  1. public class ReorderDemo3{
  2. private int x= 0;
  3. private Boolean flag = false;
  4. public void update() {
  5. x= 8; //①
  6. flag = true; //②
  7. }
  8. public void show() {
  9. if(flag) { //③
  10. // x是多少?
  11. System.out.println(x);
  12. }
  13. }
  14. }

ReorderDemo3并发运行之后,控制台所输出的x值可能是0或8。为什么x可能会输出0呢?主要原因是:update()和show()方法可能在两个CPU内核并发执行,语句①和语句②如果发生了重排序,那么show()方法输出的x就可能为0。如果输出的x结果是0,显然不是程序的正常结果。
如何确保ReorderDemo3的并发运行结果正确呢?可以通过内存屏障进行保障。Java语言没有办法直接使用硬件层的内存屏障,只能使用含有JMM内存屏障语义的Java关键字,这类关键字的典型为volatile。使用volatile关键字对实例中的x进行修饰,修改后的ReorderDemo3代码如下

  1. public class ReorderDemo3{
  2. private volatile int x= 0; //使用volatile 关键字对x进行修饰
  3. private Boolean flag = false;
  4. public void update() {
  5. x= 8; //①
  6. //volatile 要求编译器在这里插入Store Barrier写屏障
  7. flag = true; //②
  8. }
  9. public void show() {
  10. if(flag) { //③
  11. // x是多少
  12. System.out.println(x);
  13. }
  14. }
  15. }

修改后的ReorderDemo3代码使用volatile关键字对成员变量x进行修饰,volatile含有JMM全屏障的语义,要求JVM编译器在语句①的后面插入全屏障指令。该全屏障确保x的最新值对所有的后序操作是可见的(含跨CPU场景),并且禁止编译器和处理器对语句①和语句②进行重排序。

4.5 JMM详解

JMM(Java Memory Model,Java内存模型)并不像JVM内存结构一样是真实存在的运行实体,更多体现为一种规范和规则。

4.5.1 什么是Java内存模型

image.png

4.5.2 JMM与JVM物理内存的区别

image.png

4.5.3 JMM的8个操作

4.5.4 JMM如何解决有序性问题

4.5.5 volatile语义中的内存屏障

image.png
image.png

4.6 Happens-Before规则

4.6.1 Happens-Before规则介绍

4.6.2 规则1:顺序性规则

4.6.3 规则2:volatile规则

4.6.4 规则3:传递性规则

4.6.5 规则4:监视锁规则

4.6.6 规则5:start()规则

4.6.7 规则6:join()规则

4.7 volatile不具备原子性

volatile能保证数据的可见性,但volatile不能完全保证数据的原子性,对于volatile类型的变量进行复合操作(如++),其仍存在线程不安全的问题。

4.7.1 volatile变量的自增实例

  1. package com.crazymakercircle.visiable;
  2. // 省略import
  3. public class VolatileDemo
  4. {
  5. private volatile long value;
  6. @org.junit.Test
  7. public void testAtomicLong()
  8. {
  9. // 并发任务数
  10. final int TASK_AMOUNT = 10;
  11. //线程池,获取CPU密集型任务线程池
  12. ExecutorService pool = ThreadUtil.getCpuIntenseTargetThreadPool();
  13. // 每个线程的执行轮数
  14. final int TURNS = 10000;
  15. // 线程同步倒数闩
  16. CountDownLatch countDownLatch = new CountDownLatch(TASK_AMOUNT);
  17. long start = System.currentTimeMillis();
  18. for (int i = 0; i < TASK_AMOUNT; i++)
  19. {
  20. pool.submit(() ->
  21. {
  22. try
  23. {
  24. for (int j = 0; j < TURNS; j++)
  25. {
  26. value++;
  27. }
  28. } catch (Exception e)
  29. {
  30. e.printStackTrace();
  31. }
  32. //倒数闩,倒数一次
  33. countDownLatch.countDown();
  34. });
  35. }
  36. // 省略,等待倒数闩完成所有的倒数操作
  37. float time = (System.currentTimeMillis() - start) / 1000F;
  38. //输出统计结果
  39. Print.tcfo("运行的时长为:" + time);
  40. Print.tcfo("累加结果为:" + value);
  41. Print.tcfo("与预期相差:" + (TURNS * TASK_AMOUNT - value));
  42. }
  43. }

4.7.2 volatile变量的复合操作不具备原子性的原理

对于复合操作,volatile变量无法保障其原子性,如果要保证复合操作的原子性,就需要使用锁。并且,在高并发场景下,volatile变量一定需要使用Java的显式锁结合使用。