引入
Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范, 用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。
-
主内存(Main Memory)
主内存可以简单理解为计算机当中的内存,但又不完全等同。
主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。 -
工作内存(Working Memory)
工作内存可以简单理解为计算机当中的CPU高速缓存,但又不完全等同。
每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。
线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。
不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。
对于一个静态变量
static int s = 0;
线程A执行如下代码:
s = 3;
通过一系列内存读写的操作指令,线程A把静态变量 s=0 从主内存读到工作内存,再把 s=3 的更新结果同步到主内存当中。
从单线程的角度来看,这个过程没有任何问题。这时候我们引入线程B,执行如下代码:
System.out.println("s=" + s);
有可能输出 0 或 3
可以使用synchronized同步锁(影响性能)或者使用volatile关键字修饰(轻量)
volatile的实现原理
volatile可以实现内存的可见性和防止指令重排序。
通过内存屏障技术实现的。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障指令,内存屏障效果有:
- 禁止volatile 修饰变量指令的重排序
- 写入数据强制刷新到主存
- 读取数据强制从主存读取
使用原则:
- 对变量的写操作不依赖于当前值。例如 i++ 这种就不适用。
- 该变量没有包含在具有其他变量的不变式中。
volatile的使用场景不是很多,使用时需要仔细考虑下是否适用volatile,注意满足上面的二个原则。
volatile 之 可见性
volatile关键字具有许多特性,其中最重要的特性就是保证了用volatile修饰的变量对所有线程的可见性[当一个线程修改了变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值]。
比如用一个变量标识程序是否启动、初始化完成、是否停止等,如下:
public class MessageLoopHandler{
private volatile boolean shutdown = false;
//A线程执行shutdown方法
public void shutdown(){
shutdown = true;
}
//B线程检查到shutdown为true结束循环
public void doWork(){
while(!shutdown){
//应用没有停止时,继续检查是否有新消息
}
}
}
但volatile不能保证变量的原子性即无法保证线程安全。
什么时候适合用
- 运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
public class VolatileTest { public volatile static int count = 0; public static void main(String[] args) { //开启 10个线程 for (int i = 0; i < 10; i++) { new Thread( new Runnable() { public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } //每个线程当中让count值自增 100次 for (int j = 0; j < 100; j++) { count++; } } } ).start(); } try { Thread.Weep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System, out.print("count" + count); //最终count的结果值未必是1000,有可能小于1000。 } }
- 变量不需要与其他的状态变量共同参与不变约束。
volatile static int start = 3; volatile static int end = 6; 线程A执行如下代码: while (start < end){ //do something } 线程B执行如下代码: start+=3; end+=3; //这种情况下,一旦在线程A的循环中执行了线程B,start有可能先更新成6, //造成了一瞬间 start == end,从而跳出while循环的可能性。
volatile 之 有序性 (阻止指令重排)
public class Singleton2 {
private static Singleton2 instance;
public static Singleton2 getInstance() {
//双重检测机制
if (instance == null) {
//同步锁
synchronized (Singleton2.class) {
//双重检测机制
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
}
已上代码存在问题,可能会得到一个没有初始化完成的对象
JVM编译器的指令重排,可能会得到一个没有初始化的对象
java中简单的一句 instance = new Singleton
,会被编译器编译成如下JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。
此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。
如何避免这一情况呢?我们需要在instance对象前面增加一个修饰符volatile。