且看如下代码,两个线程AB分别执行doSth1和doSth2函数,输出多少种结果:
maybe:
flag = true a = 0
flag = true a = 1
如果代码改成如下呢,函数doSth2中是否可以进入 a = a - 1的计算语句,最后结果又是多少?
maybe:
flag = true a = 1
最后的实验结果,还有可能的结果如下:
flag = true a = 0
flag = true a = -2
到底是什么导致这种结果(大写的问号❓)带着这种好奇,我们直接进入这次分享主题,Java内存模型。
敲黑板!重点来了!!!什么是Java内存模型?
内存模型,在特定的操作协议下对特定的内存或高速缓存进行读写访问的过程抽象。
Java内存模型,通过屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果的过程抽象。
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。(变量主要指实例字段、静态字段和构成数组对象的元素)
Java 线程之间的通信由 Java 内存模型(本文简称为 JMM)控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读 / 写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java 内存模型的抽象示意图如下:
从上图来看,线程 A 与线程 B 之间如要通信的话,必须要经历下面 2 个步骤:
下面通过示意图来说明这两个步骤:
从整体来看,这两个步骤实质上是线程 A 在向线程 B 发送消息,而且这个通信过程必须要经过主内存。JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 java 程序员提供内存可见性保证。
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
as-if-serial语义的意思是,所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变,编译器、runtime、处理器都必须遵守as-if-serial语义。注意as-if-serial只保证单线程环境,多线程环境下无效。
下面我们用一个简单的示例来说明:
int a = 1 ; //A
int b = 2 ; //B
int c = a + b; //C
A、B、C三个操作存在如下关系:A、B不存在数据依赖关系,A和C、B和C存在数据依赖关系,因此在进行重排序的时候,A、B可以随意排序,但是必须位于C的前面,执行顺序可以是A –> B –> C或者B –> A –> C。但是无论是何种执行顺序最终的结果C总是等于3。
as-if-serail语义把单线程程序保护起来了,它可以保证在重排序的前提下程序的最终结果始终都是一致的。
先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
程序次序规则(Program Order Rule):在一个线程中,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。
管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调是同一个锁,“后面”是指时间的先后顺序。
volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,如join()、isAlive()可以检测到线程已经终止。
线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt()方法检测到是否有中断发生。
对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C。
flag = true a = 0
flag = true a = -2
附--测试源码:
package com.kylefu.tk.jmm;
/**
* 多线程 代码重排序例子
*/
public class MutilThread {
int a = -1;
boolean flag = false;
public void doSth1(){
a = 1; // A
flag = true; // B A与B不涉及依赖,A B之间只涉及程序次序原则,A的结果对B可见,指令重排序导致B先执行,不影响单线程结果。
}
public void doSth2(){
// 正常业务逻辑
/*if(a == -1 && flag){
System.out.println("beg:" + a + " " + flag);
}*/
/*if(flag){
ret = a - 1;
}*/
// 重排序验证,当前flag为true,且a还是初始值
/*if(a == -1 && flag){
System.out.println("beg:" + a + " " + flag);
}*/
// 指令重排序导致多线程之间计算丢失 --测试结果:160740:-2 true
if(a == -1 && flag){
a = a - 1;
}
}
public static void main(String[] args){
MutilThread mt = null;
int i = 0;
while(true){
i++;
mt = new MutilThread();
ThreadA at = new ThreadA(mt);
ThreadB bt = new ThreadB(mt);
at.start();
bt.start();
try{
at.join();
bt.join();
}catch (Exception e){
e.printStackTrace();
}
if(mt.a == 0 ){
System.out.println(i + ":a=" + mt.a + " flag=" + mt.flag );
}
if(mt.a == -2 ){
System.out.println(i + ":a=" + mt.a + " flag=" + mt.flag );
break;
}
}
}
}
class ThreadA extends Thread{
public ThreadA(MutilThread thread){
this.thread = thread;
}
MutilThread thread = null;
public void run(){
thread.doSth1();
}
}
class ThreadB extends Thread{
public ThreadB(MutilThread thread){
this.thread = thread;
}
MutilThread thread = null;
public void run(){
thread.doSth2();
}
}