深入理解java虚拟机(22):volatitle变量规则
volatitle变量对所有线程立即可见,对volatitle变量的操作立刻能反应到其他的线程里面。volatitle变量在线程工作内存里面也存在不一致性,但由于每次使用前要刷新,执行引擎看不到不一致的情况,但是java里面的运算并非原子操作,volatitle变量的运算在并发下一样是不安全的。如下代码
package org.xiaofeiyang.classloader;
/**
* @author: yangchun
* @description:
* @date: Created in 2019-12-04 9:22
*/
public class VolatileTest {
public static volatile int race =0;
public static void increase(){
race++;
}
private static final int THREADS_COUNT =20;
public static void main(String[] args){
Thread[] threads = new Thread[THREADS_COUNT];
for(int i=0;i<THREADS_COUNT;i++){
threads[i] =new Thread(new Runnable() {
@Override
public void run() {
for(int j=0;j<1000;j++){
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount()>1){
Thread.yield()
}
System.out.println(race);
}
}
这段代码的部分字节码如下
iconst_1,iadd这些指令不是原子的有可能你做这些操作的时候其他线程已经将值修改了因此不能得到正确的结果。putstatic有可能将较小的值放回主内存。由于volatitle只保证可见性,所以在以下场景不适合使用
运算结果并不依赖变量的当前值或者能确保只有单一的线程修改变量的值。变量不需要与其他的状态变量共同参与不变约束。如下代码就很合适:
volatile boolean shutdownRequested;
public void shutdown(){
shutdownRequested = true;
}
public void doWork(){
while (!shutdownRequested){
}
使用volatile变量的第二个语义是禁止指令重排优化,普通变量只能保证所有依赖赋值结果的地方都能获得到正确的结果,而不能保证变量赋值的操作的顺序与程序代码中的顺序一致。可以看看下面一个代码指令重排
Map cofigOptions;
char[] configText;
volatile boolean initialized = false;
private void test(String fileName){
cofigOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText,cofigOptions);
initialized = true;
while (!initialized){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
doSomethingWithConfig();
}
private void doSomethingWithConfig() {
}
private void processConfigOptions(char[] configText, Map cofigOptions) {
}
private char[] readConfigFile(String fileName) {
return new char[10];
}
这个代码就可能发送initialized赋值提前了。
再看看如下代码和它的反汇编代码
可以看到movb%eax,0x150(%esi)后面多了一个lock add操作相当于多了一个内存屏障。如果有两个cpu同事操作一块内存,其中一个观察另外一个就需要内存屏障。add1$0x0,(%esp)将寄存器值加0。lock前缀它作用是使得本cpu的cache写入内存,该写入操作也会使得其他cpu的cahe失效。
那它是如何禁止指令重排序的呢
cpu不是任意指令都可以从排序,例如指令1取a地址中的值,指令2把a的值乘以2,这种指令之间相互依赖就不能重新排序。lock add1$0x0,(%esp)把修改同步到主内存,意味着之前所有操作都已经完成,便形成了一到指令重排序无法逾越的内存屏障。
java内存模型对volatitle变量定义的特殊规则。T表示一个线程,V和W表示两个volatitle变量。那么read,load,use,assign,store write这几个操作必须满足下列规则。
线程对t进行use前必须load,线程t对v变量的操作read,load,use可以看做是相互关联的。线程t对变量执行assign时必须store,可以将assgin store write看做是关联的一起出现的。
假定动作A是线程t对变量V实施的use或者assign操作,动作f是相对应的load或者store操作,假定动作P是F相对应的read或者write操作。假定动作B是线程t对变量V实施的use或者assign操作,动作G是相对应的load或者store操作,假定动作Q是G相对应的read或者write操作。如果A先于B,那么F先于G,P先于Q
java内存模型允许虚拟机将64位数据的读写操作分成两次32位的操作来进行,但是虚拟机都会把64位数据读写实现为原子操作
2、原子性、可见性、有序性和
1)原子性主要又数据操作指令是原子性。更大范围的可以使用lock,unlock,虽然没有放开给用,但是对应的就是moniterenter和moniterexit这两个隐式操作,反应到java代码的synchronize关键字。
2)可见性,一个线程修改了变量值,其他线程立马就知道,除了volatitle还有sychronized和final。sychronized的unlock操作之前,会把数据同步回主内存。
3)有序性
3、先行发生原则
先行发生是java内存模型中定义的两项操作之间的关系,如果A操作先行于B操作发生,影响也就是说A操作的影响B操作能观察到。下面代码
线程A中操作i=1;线程B中操作j=i;线程C中操作i=2,如果线程A中操作i=1先于线程B中的j=i,那么线程b中j一定等于1.C线程和B线程没有先行关系,所以j有可能是2或者1.
程序次序规则,程序代码顺序,书写在前面的操作先行于书写在后面的操作,控制流顺序不是代码顺序还要考虑分支和循环。
管程锁定规则,一个unlock操作一定发生在同一个锁的lock操作之后。
volatitle规则,对volatitle操作一定写一定先于后面的读操作
线程启动规则,start方法先于所有操作
线程终止规则,所有操作都先于线程终止检查
线程中断规则,对线程interrupt方法调用先于线程终止检查到中断事件发生。
对象终结规则,一个对象的初始化完成,先行发生于它的finalize的方法开始。
传递性: