JMM 简介及 volatile 的说明
个人博客地址:https://huanxi.pub
为了屏蔽各个操作系统和硬件的差异,使得 Java 程序在所有平台下都能达到一致的内存访问效果,所以 Java 虚拟机定义了一种 Java 内存模型。
Java 内存模型的作用
我们写代码,说到底就是在操作内存。
Java 内存模型主要定义了程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量的底层细节。(这里的变量不包括局部变量和方法参数,因为那是线程私有的,不会产生竞争)
Java 虚拟机规定所有的变量都存储在主内存(Main Memory),每个线程都有自己的工作线程(Work Memory)。
线程的工作内存中保存了使用到的变量的主内存副本拷贝,线程对变量的操作是在自己的工作内存中,而不能直接对主内存的变量进行读取赋值。
不同线程之间无法直接访问对方工作内存中的变量,需要通过主内存来进行传递。
线程、主内存、工作内存之间的关系如下图所示
main_work_memory
volatile 关键字
volatile 变量具备两种特性,其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。其二 volatile 禁止了指令重排。
虽然 volatile 变量具有可见性和禁止指令重排序,但是并不能说 volatile 变量能确保并发安全。
public class VolatileTest {
public static volatile int a = 0;
public static final int THREAD_COUNT = 20;
public static void increase() {
a++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 1000; i++) {
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(a);
}
}
按照我们的预期,它应该返回 20000 ,但是很可惜,该程序的返回结果几乎每次都不一样。
问题主要出在 a++
上,复合操作并不具备原子性, 虽然这里利用 volatile 定义了 a
,但是在做 a++
时, 先获取到最新的 a
值,比如这时候最新的可能是 50,然后再让 a
增加,但是在增加的过程中,其他线程很可能已经将 a
的值改变了,或许已经成为 52、53 ,但是该线程作自增时,还是使用的旧值,所以会出现结果往往小于预期的 2000。如果要解决这个问题,可以对 increase()
方法加锁。
volatile 适用场景
volatile
适用于程序运算结果不依赖于变量的当前值,也相当于说,上述程序的 a 不要自增,或者说仅仅是赋值运算,例如 boolean flag = true
这样的操作。
volatile boolean shutDown = false;
public void shutDown() {
shutDown = true;
}
public void doWork() {
while (!shutDown) {
System.out.println("Do work " + Thread.currentThread().getId());
}
}
当调用 shutDown()
方法时,能保证所有的线程都停止工作。
指令重排
int a = 1;
int b = 2;
int c = a * b;
CPU
为了提升效率,允许将多条指令进行重新排序。
比如上述 就有可能先执行
int b = 2;
然后执行
int a = 1;
但是 int c = a * b
是不会进行重排的,它必须在 a、b 之后,因为 c 对 a、b 有所依赖。
上面的代码,在单线程中,即使经过指令重排,但是并不会影响最终的结果,所以是不会出问题的,但是在多线程中,就会引发问题。
public class Singleton {
private static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton(); //非原子操作
}
}
}
return instance;
}
}
看似简单的一段赋值语句:instance= new Singleton(),但是很不幸它并不是一个原子操作,其实际上可以抽象为下面几条JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:
memory =allocate();
//1:分配对象的内存空间
instance =memory;
//3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory);
//2:初始化对象
可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。
在线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值给instance引用,恰好另一个线程进入方法判断instance引用不为null,然后就将其返回使用,导致出错。
在 instance
加上 volatile
即可防止指令重排。
本文参考:《深入理解 Java 虚拟机第二版》
最后一个例子及说明摘自:Java并发:volatile内存可见性和指令重排