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位数据读写实现为原子操作
原文:https://www.cnblogs.com/xiaofeiyang/p/11981465.html