Java内存模型
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(仅是虚拟机内存的一部分),每条线程还有自己的工作内存(Working Memory)。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程变量值的传递均需要通过主内存来完成。关系如下图。
内存间交互操作
操作 | 作用 |
---|---|
lock(锁定) | 作用于主内存的变量,它把一个变量标识为一条线程独占的状态 |
unlock(解锁 ) | 作用于主内存的变量,它把一个处于锁定状态的变量释放出来释放后的变量才可以被其他线程锁定 |
read(读取) | 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用 |
load(载入) | 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中 |
use(使用) | 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时执行这个操作 |
assign(赋值) | 作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 |
store(存储) | 作用于工作内存的变量,它把工作内存一个变量的值传递送到主内存中,以便随后的wirte操作使用 |
wtire(写入) | 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中 |
Volatile特性
-
保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的,而普通变量不能做到这一点,变量值在线程间传递均需要通过主内存来完成。
-
具有可见性、有序性,但是不具有原子性。
-
强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。
-
禁止指令重排序优化
-
volatile变量读操作的性能消耗与普通变量几乎没什么差别,但是写操作上可能会慢上一些,因为它需要在本地代码上插入许多内存屏障(Memory Barrier或Memory Fence)指令来保证处理器不发生乱序执行。
不具有原子性所带来的问题
volatile变量对所有线程是立即可见的,对volatile变量所有的写操作都能立刻反应到其他线程之中,换句话说,volatile变量在各个线程中是一致的,但是基于volatile变量的运算在并发下并不一定安全。做个简单的测试。
public class Run {
public static volatile int race = 0;
public static void increase() {
race++;
}
//线程数
private static final int THREADS_COUNT = 20;
public static class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
}
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new MyThread();
threads[i].start();
}
//等待所有线程累加结束
//这里的2是因为在IntelliJ IDEA的环境下
while (Thread.activeCount() > 2) {
Thread.yield();
System.out.println(race);
}
}
}
运行结果
分析
这段代码发起了20个线程,每个线程对race变量进行10000次自增,如果正确并发执行的话,最后输出结果应该是200000,但是运行结果却并不是。对于用volatile修饰的变量,虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
在某些JVM中,race++的操作要分成下面3步:
(1)取得原有race值
(2)计算race+1
(3)对race进行赋值
也就是说对于用volatile修饰的变量,在取得原有值的时候可以保证是最新的,但是由于race++分为3个步骤,有可能在线程A刚刚取得值的时候,线程B完成了race+1的操作,此时线程A的数据其实就过时了,最后线程A又把race值存储回主内存,线程B白做工,导致了最后总体值偏小。
最后再看下原理图
如何应用可见性
下面的场景就很适合使用volatile变量来控制并发,当shutdown()方法被调用时,能保证所有线程中执行的doWork()方法都立即停下来。
volatile boolean shutdownRequested;
public void shutdown(){
shutdownRequested = true;
}
public void doWork(){
while(!shutdownRequested){
//do stuff
}
}
禁止指令重排序
使用volatile变量的第二个语义是禁止指令重排序,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码的执行顺序一致。看一段伪代码。
Map configOptions;
char [] configText;
//此变量必须定义为volatile
volatile boolean initialized = false;
//假设以下代码在线程A中执行
//模拟读取配置信息,当读取完成后
//将initialized设置为true来通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(filename);
processConfigOptions(configText, configOptions);
initialized=true;
//假设以下代码在线程B中执行
//等待initialized为true,代表线程A已经把配置信息初始化完成
while(!initialized){
sleep();
}
//使用线程A中初始化好的配置信息
doSomethingWithConfig();
其中描述的场景是十分常见,只是我们再处理配置文件时一般不会出现并发而已。如果定义initialized变量时没有使用volatile修饰,就可能会由于指令重排序优化,导致位于线程A最后一句的代码initialized=true;
被提前执行,这样线程B中使用配置信息的代码就可能出现错误,而volatile关键字则可以避免此类情况发生。
《Java多线程编程核心技术》
《深入理解Java虚拟机JVM高级特性与最佳实践》