本文大纲
1. 重排序
2. volatile 的作用
3. happens-before
3.1 线程内的 happens-before
3.2 线程间的 happens-before
4. JMM 底层实现原理
1. 重排序
我们来看一段代码:
- public class JmmTest implements Runnable {
- int a = 0;
- int b = 0;
- public void method1() {
- int r2 = a;
- b = 1;
- System.out.println("r2:" + r2);
- }
- public void method2() {
- int r1 = b;
- a = 2;
- System.out.println("r1:" + r1);
- }
- public static void main(String[] args) {
- JmmTest tmmTest = new JmmTest();
- Thread t1 = new Thread(tmmTest, "t1");
- Thread t2 = new Thread(tmmTest, "t2");
- t1.start();
- t2.start();
- }
- @Override
- public void run() {
- if ("t1".equals(Thread.currentThread().getName())) {
- method1();
- } else {
- method2();
- }
- }
- }
上面这段代码中, r1,r2 的结果可能会有如下三种情况:
- r1=0,r2=0;
- r1=0,r2=2;
- r1=1,r2=0.
- (注: 本文中, 在非代码片段中的 "=" 均念作等于, 非赋值操作.)
但是, 还存在一种看起来不可能的结果 r1=1,r2=2. 造成这种结果的原因可能有:
即时编译器的重排序;
处理器的乱序执行.
即时编译器和处理器可能将代码中没有数据依赖的代码进行重排序. 但如果代码存在数据依赖关系, 那么这部分代码不会被重排序. 上面的示例代码中, method1 方法中对 r2,b 的赋值就不存在依赖关系, 所以可能会发生重排序. method2 方法同理.
代码被重排序后, 可能存在如下的顺序:
- public void method1() {
- b = 1; // 重排序后, method1 先对 b 进行赋值
- int r2 = a;
- System.out.println("r2:" + r2);
- }
- public void method2() {
- a = 2; // 重排序后, method2 先对 a 进行赋值
- int r1 = b;
- System.out.println("r1:" + r1);
- }
这种情况下, 当一个线程对 a,b 其中的一个变量进行赋值后, CPU 切换到另外一个线程对另外一个变量进行赋值, 就会出现 r1=1,r2=2 的结果.
需要指出的是, 在单线程情况中, 即使经过重排序的代码也不会影响代码输出正确的结果. 因为即时编译器和处理器会遵守 as-if-serial 语义, 即在单线程情况下, 要给程序一个顺序执行的假象, 即使经过重排序的代码的执行结果要和代码顺序执行的结果一致. 但是, 在多线程的情况下, 即时编译器和处理器是不会对经过重排序的代码做任何保证. 同时, Java 语言规范将这种归咎于我们的程序没有做出恰当的同步操作, 即我们没有显式地对数据加上 volatile 声明或者其他加锁操作.
2. volatile 的作用
禁止指令重排序;
线程间共享数据的可见性.
3. happens-before
为了让我们的代码免于上述问题的困扰, Java 5 明确定义了 Java 内存模型. 其中最为重要的一个概念就是 happens-before.
happens-before 是用于描述两个操作间数据的可见性的. 如果 X happens-before Y, 那么 X 的结果对于 Y 可见. 下面我将讲述单一线程和多线程情况下的 happens-before.
3.1 线程内的 happens-before
在同一个线程中, 字节码的先后顺序暗含了 happens-before 的关系. 在代码中靠前的代码 happens-before 靠后的代码. 但是, 这并不意味前面的代码一定比后面的代码先执行, 如果后面的代码没有依赖于前面代码的结果, 那么它们可能会被重排序, 从而后面的代码可能会先执行, 就像文中前面提到的一样.
3.2 线程间的 happens-before
先重点关注下面的 happens-before 关系中标红的部分.
volatile 字段的写操作 happens-before 之后 (这里指时钟顺序先后) 对同一字段的读操作;
解锁操作 happens-before 之后 (这里指时钟顺序先后) 对同一把锁的加锁操作;
线程的启动操作(即 Thread.starts()) happens-before 该线程的第一个操作;
线程的最后一个操作 happens-before 它的终止事件(即其他线程通过 Thread.isAlive() 或 Thread.join() 判断该线程是否中止);
线程对其他线程的中断操作 happens-before 被中断线程所收到的中断事件(即被中断线程的 InterruptedException 异常, 或者第三个线程针对被中断线程的 Thread.interrupted 或者 Thread.isInterrupted 调用);
构造器中的最后一个操作 happens-before 析构器的第一个操作;
happens-before 具备传递性.
上文我们的代码中, 除了有线程内的 happens-before 关系, 没有定义其他任何线程间的 happens-before 关系, 并且 method1 和 method2 两个方法中的赋值操作没有数据依赖关系, 所以可能会发生重排序, 从而得到 r1=1,r2=2 的结果. 根据线程间的 happens-before 关系, 我们可以对 a 或者 b 加上 volatile 修饰符来避免这个问题.
以给成员变量 b 加上 volatile 修饰符为例:
- int a = 0;
- volatile int b = 0; // 加上 volatile 修饰符
- public void method1() {
- int r2 = a;
- b = 1;
- System.out.println("r2:" + r2);
- }
- public void method2() {
- int r1 = b;
- a = 2;
- System.out.println("r1:" + r1);
- }
一旦 b 加上了 volatile, 即时编译器和 CPU 需要考虑到多线程 happens-before 关系, r2=a 和 b=1 将不能自由地重排序, 所以第 r2 的赋值操作先于 b 的赋值操作执行, 同时, 根据 volatile 字段的写操作 happens-before 之后对同一字段的读操作, 所以 b 的赋值操作先于 r1 的赋值操作执行. 这也就意味着, 当对 a 进行赋值时, 对 r2 的赋值操作已经完成了. 因此, 在 b 为 volatile 字段的情况下, 程序不可能出现 r1=1,r2=2 的情况.
总之, 解决这种问题的关键在于构造一个线程间的 happens-before 关系.
4. JMM 底层实现原理
Java 内存模型是通过内存屏障 (memory barrier) 来禁止重排序的. 这些内存屏障会限制即时编译器的重排序操作. 以 volatile 字段访问为例, 所插入的内存屏障将不允许 volatile 字段写操作之前的内存访问被重排序至其之后; 也将不允许 volatile 字段读操作之后的内存访问被重排序至其之前.
在碰到内存写操作时, 处理器并不会等待该指令结束, 而是直接开始下一指令, 并且依赖于写缓存将更改的数据同步至主内存 (main memory) 之中. 强制刷新写缓存, 将使得当前线程写入 volatile 字段的值(以及写缓存中已有的其他内存修改), 同步至主内存之中.
由于内存写操作同时会无效化其他处理器所持有的, 指向同一内存地址的缓存行, 因此可以认为其他处理器能够立即见到该 volatile 字段的最新值.
参考文章
极客时间《深入拆解 Java 虚拟机》专栏的《Java 内存模型》.
来源: https://www.cnblogs.com/pedlar/p/10703705.html