4.1 CPU物理缓存结构
4.2 并发编程的三大问题
4.2.1 原子性问题
就是“不可中断的一个或一系列操作”,是指不会被线程调度机制打断的操作。这种操作一旦开始,就一直运行到结束,中间不会有任何线程的切换。
class CounterSample{int sum = 0;public void increase() {sum++; //①}}
4.2.2 可见性问题
一个线程对共享变量的修改,另一个线程能够立刻可见,我们称该共享变量具备内存可见性。
要想解决多线程的内存可见性问题,所有线程都必须将共享变量刷新到主存,一种简单的方案是:使用Java提供的关键字volatile修饰共享变量。
所有的Object实例、Class实例和数组元素都存储在JVM堆内存中,堆内存在线程之间共享,所以存在可见性问题。
4.2.3 有序性问题
所谓程序的有序性,是指程序按照代码的先后顺序执行。如果程序执行的顺序与代码的先后顺序不同,并导致了错误的结果,即发生了有序性问题。
package com.crazymakercircle.visiable;// 省略importpublic class InstructionReorder {private volatile static int x = 0, y = 0;private static int a = 0, b = 0;public static void main(String[] args) throws InterruptedException {int i = 0;for (;;) {i++;x = 0;y = 0;a = 0;b = 0;Thread one = new Thread(new Runnable() {public void run() {a = 1; //①x = b; //②}});Thread other = new Thread(new Runnable() {public void run() {b = 1; //③y = a; //④}});one.start();other.start();one.join();other.join();String result = "第" + i + "次 (" + x + "," + y + ")";if (x == 0 && y == 0) {System.err.println(result);}}}}
4.3 硬件层的MESI协议原理
4.3.1 总线锁和缓存锁
为了解决内存的可见性问题,CPU主要提供了两种解决办法:总线锁和缓存锁。
- 总线锁

- 缓存锁
4.3.2 MSI协议
4.3.3 MESI协议及RFO请求
- 初始阶段:开始时,缓存行没有加载任何数据,所以它处于“I状态”

- 本地写(Local Write)阶段:如果CPU内核写数据到处于“I状态”的缓存行,缓存行的状态就变成“M状态”。
4.3.4 volatile的原理
在正常情况下,系统操作并不会校验共享变量的缓存一致性,只有当共享变量用volatile关键字修饰了,该变量所在的缓存行才被要求进行缓存一致性的校验。
从volatile关键字的汇编代码出发分析一下volatile关键字的底层原理,参考如下示例代码:
package com.crazymakercircle.visiable;public class VolatileVar{//使用volatile保障内存可见性volatile int var = 0;public void setVar(int var){System.out.println("setVar = " + var);this.var = var;}public static void main(String[] args){VolatileVar var = new VolatileVar();var.setVar(100);}}
4.4 有序性与内存屏障
在编译器和CPU都进行指令的重排优化时,可以通过在指令间插入一个内存屏障指令,告诉编译器和CPU,禁止在内存屏障指令前(或后)执行指令重排序。
4.4.2 As-if-Serial规则
public class ReorderDemo{public static void main(String[] args) {int a=1; //①int b=2; //②int c=a+b; //③}}
为了保证As-if-Serial规则,Java异常处理机制也会为指令重排序做一些特殊处理。下面是一段非常简单的Java异常处理示例代码:
public class ReorderDemo2{public static void main(String[] args) {int x, y;x = 1;try {x = 2; //①y = 0/0; //②} catch (Exception e) { //③} finally {System.out.println("x = " + x);}}}
As-if-Serial规则只能保障单内核指令重排序之后的执行结果正确,不能保障多内核以及跨CPU指令重排序之后的执行结果正确。
4.4.3 硬件层面的内存屏障
- 硬件层的内存屏障定义
- 硬件层的内存屏障的作用
- 内存屏障的使用示例
下面是一段可能乱序执行的代码:
public class ReorderDemo3{private int x= 0;private Boolean flag = false;public void update() {x= 8; //①flag = true; //②}public void show() {if(flag) { //③// x是多少?System.out.println(x);}}}
ReorderDemo3并发运行之后,控制台所输出的x值可能是0或8。为什么x可能会输出0呢?主要原因是:update()和show()方法可能在两个CPU内核并发执行,语句①和语句②如果发生了重排序,那么show()方法输出的x就可能为0。如果输出的x结果是0,显然不是程序的正常结果。
如何确保ReorderDemo3的并发运行结果正确呢?可以通过内存屏障进行保障。Java语言没有办法直接使用硬件层的内存屏障,只能使用含有JMM内存屏障语义的Java关键字,这类关键字的典型为volatile。使用volatile关键字对实例中的x进行修饰,修改后的ReorderDemo3代码如下
public class ReorderDemo3{private volatile int x= 0; //使用volatile 关键字对x进行修饰private Boolean flag = false;public void update() {x= 8; //①//volatile 要求编译器在这里插入Store Barrier写屏障flag = true; //②}public void show() {if(flag) { //③// x是多少System.out.println(x);}}}
修改后的ReorderDemo3代码使用volatile关键字对成员变量x进行修饰,volatile含有JMM全屏障的语义,要求JVM编译器在语句①的后面插入全屏障指令。该全屏障确保x的最新值对所有的后序操作是可见的(含跨CPU场景),并且禁止编译器和处理器对语句①和语句②进行重排序。
4.5 JMM详解
JMM(Java Memory Model,Java内存模型)并不像JVM内存结构一样是真实存在的运行实体,更多体现为一种规范和规则。
4.5.1 什么是Java内存模型
4.5.2 JMM与JVM物理内存的区别
4.5.3 JMM的8个操作
4.5.4 JMM如何解决有序性问题
4.5.5 volatile语义中的内存屏障
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变量的自增实例
package com.crazymakercircle.visiable;// 省略importpublic class VolatileDemo{private volatile long value;@org.junit.Testpublic void testAtomicLong(){// 并发任务数final int TASK_AMOUNT = 10;//线程池,获取CPU密集型任务线程池ExecutorService pool = ThreadUtil.getCpuIntenseTargetThreadPool();// 每个线程的执行轮数final int TURNS = 10000;// 线程同步倒数闩CountDownLatch countDownLatch = new CountDownLatch(TASK_AMOUNT);long start = System.currentTimeMillis();for (int i = 0; i < TASK_AMOUNT; i++){pool.submit(() ->{try{for (int j = 0; j < TURNS; j++){value++;}} catch (Exception e){e.printStackTrace();}//倒数闩,倒数一次countDownLatch.countDown();});}// 省略,等待倒数闩完成所有的倒数操作float time = (System.currentTimeMillis() - start) / 1000F;//输出统计结果Print.tcfo("运行的时长为:" + time);Print.tcfo("累加结果为:" + value);Print.tcfo("与预期相差:" + (TURNS * TASK_AMOUNT - value));}}
4.7.2 volatile变量的复合操作不具备原子性的原理
对于复合操作,volatile变量无法保障其原子性,如果要保证复合操作的原子性,就需要使用锁。并且,在高并发场景下,volatile变量一定需要使用Java的显式锁结合使用。

