目录
一、前言
1. 作为我们程序猿不想写出 bug 影响 KPI,所以希望内存模型易于理解、易于编程。这就需要基于一个强内存模型来编写代码
2. 作为编译器和处理器不想让外人说它处理速度很慢,所以希望内存模型对他们束缚越少越好,可以由他们擅自优化,这就需要基于一个弱内存模型
既然不能完全禁止缓存和编译优化,那就按需禁用缓存和编译优化,按需就是要加一些约束,约束中就包括了上一篇文章简单提到 过的 volatile,synchronized,final 三个关键字,同时还有你可能听过的 Happens-Before 原则(包含可⻅性和有序性的约束)
1. 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
2. 对于不会改变程序执行结果的重排序, JMM对编译器和处理器不做要求 (JMM 允许这种重排序)
二、Happens-before
Happens-before 规则主要用来约束两个操作,两个操作之间具有 happens-before 关系, 并不意味着前一个操作必须要在后一个操作之前执行,happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可⻅
先来看一小段代码带你逐步走进 Happen-Befores 原则,看看是怎样 用该原则解决 可⻅性 和 有序性 的问题
class ReorderExample {
int x = 0;
boolean flag = false;
public void writer() {
x = 42; //1
flag = true; //2
}
public void reader() {
if (flag) { //3
System.out.println(x); //4
}
}
}
假设 A 线程执行 writer 方法,B 线程执行 reader 方法,打印出来的 x 可能会是 0, 因为代码 1 和 2 没有数据依赖关系,所以可能被重排序 。线程 A 将 flag = true 写入但没有为 x 重新赋值时,线程 B 可能就已经打印了x是 0
flag = true; //2
x=42; //1
那么为 flag 加上 volatile 关键字试一下
volatile boolean flag = false;
1. 程序顺序性规则
一个线程中的每个操作, happens-before 于该线程中的任意后续操作 第一感觉 这个原则是一个在理想状态下的"废话",并且和上面提到的会出现重排序的情况 是矛盾的,注意这里是一个线程中的操作,其实隐含了「as-if-serial」语义: 说白了就是只要执行结果不被改变,无论怎么"排序",都是对的
2. volatile变量规则
对一个 volatile 域的写, happens-before 于任意后续对这个 volatile 域的读
从这个表格最后一列可以看出
如果第二个操作为 volatile 写,不管第一个操作是什么,都不能重排序,这就确保了volatile 写之前的操作不会被重排序到 volatile 写之后。拿上面的代码来说,代码 1 和 2 不会被重排序到代码 3 的后面,但代码 1 和 2 可能被重排序 (没有依赖也不会影响到执行结果)
从这个表格的倒数第二行可以看出
如果第一个操作为 volatile 读,不管第二个操作是什么,都不能重排序,这确保了 volatile 读之后的操作不会被重排序到 volatile 读之前。拿上面的代码来 说,代码 4 是读取 volatile 变量,代码 5 和 6 不会被重排序到代码 4 之前
3. 传递性规则
如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C
- x =42 和 y = 50 Happens-before flag = true , 这是规则 1
- 写变量(代码 3) flag=true Happens-before 读变量(代码 4) if(flag) ,这是规则2
根据规则 3传递性规则, x =42 Happens-before 读变量 if(flag)
4. 监视器锁规则
对一个锁的解锁 happens-before 于随后对这个锁的加锁
public class SynchronizedExample {
private int x = 0;
public void synBlock(){
// 1.加锁
synchronized (SynchronizedExample.class){
x = 1; // 对x赋值
}
// 3.解锁
// 1.加锁
public synchronized void synMethod(){
x = 2; // 对x赋值
}
// 3. 解锁
}
先获取锁的线程,对 x 赋值之后释放锁,另外一个再获取锁,一定能看到对 x 赋值的改动,就是这么简单
5. start()规则
如果线程 A 执行操作 ThreadB.start() (启动线程B), 那么 A 线程的 ThreadB.start() 操作 happens-before 于线程 B 中的任意操作,也就是说,主线程 A 启动子线程 B 后,子线程 B 能看到主线程在启动子线程 B 前的操作
public class StartExample {
private int x = 0;
private int y = 1;
private boolean flag = false;
public static void main(String[] args) throws InterruptedException {
StartExample startExample = new StartExample();
Thread thread1 = new Thread(startExample::writer, "线程1");
startExample.x = 10;
startExample.y = 20;
startExample.flag = true;
thread1.start();
System.out.println("主线程结束");
}
public void writer(){
System.out.println("x:" + x );
System.out.println("y:" + y );
System.out.println("flag:" + flag );
}
}
线程 1 看到了主线程调用 thread1.start() 之前的所有赋值结果, 运行结果:
主线程结束
x:10
y:20flag:true
6. join()规则
如果线程 A 执行操作 ThreadB.join() 并成功返回, 那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join() 操作成功返回,和 start 规则刚好相反,主线程 A 等待子线程 B 完成,当子线程 B 完成后,主线程能够看到子线程 B的赋值操作
public class JoinExample {
private int x = 0;
private int y = 1;
private boolean flag = false;
public static void main(String[] args) throws InterruptedException {
JoinExample joinExample = new JoinExample();
Thread thread1 = new Thread(joinExample::writer, "线程1");
thread1.start();
thread1.join();
System.out.println("x:" + joinExample.x );
System.out.println("y:" + joinExample.y );
System.out.println("flag:" + joinExample.flag );
System.out.println("主线程结束");
}
public void writer(){
this.x = 100;
this.y = 200;
this.flag = true;
}
}
运行结果:
x:100
y:200
flag:true
主线程结束
三、总结
Happens-before 重点是解决前一个操作结果对后一个操作可⻅
start 和 join 规则也是解决主线程与子线程通信的方式之一
从内存语义的⻆度来说, volatile 的 写-读 与锁的 释放-获取 有相同的内存效果; volatile写和锁的释放有相同的内存语义; volatile读与锁的获取有相同的内存语义, volatile 解决的是可⻅性问题,synchronized 解决的是原子性问题
文章评论