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;
// 省略import
public 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;
// 省略import
public class VolatileDemo
{
private volatile long value;
@org.junit.Test
public 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的显式锁结合使用。