volatile是Java虚拟机提供的轻量级同步机制,保证任意线程都能看到其他线程对该变量最后写入的值。
1 保证可见性
private static volatile int C = 0;
@Test
public void test1() throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
C=10;
}
}).start();
while(C==0){
}
System.out.println("main线程执行完成");
}
如上中的变量C,如果不用volatile标识,那么main线程的本地内存空间中的C=0,不会从主内存中获取值。
2 不保证原子性
//---------------测试volatile不保证原子性
private static volatile int C11=0;
@Test
public void test11(){
for (int i = 0; i < 10; i++) {
fixedThreadPool.submit(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
C11++;
}
}
});
}
fixedThreadPool.shutdown();
while (true) {
if (fixedThreadPool.isTerminated()) {
break;
}
}
//9958
System.out.println("run over C11="+C11);
}
volatile在执行++的时候,第一步从主内存中获取值,第二步写入线程本地内存空间,第三不执行加1操作,第三步将结果写入到主内存中。
如下代码,通过 javap -c 编译后的字节码
public class VolatilePojo {
private volatile int id=0;
public void addId(){
id++;
}
}
通过 javap -c 编译后的字节码
上面的结果中总是小于的10000是因为从主存获取值后,执行加1的操作之前,其他线程可能往主存中写入了值,这样就导致执行加1操作的值是过期的数据,还有可能是出现了写覆盖,比如第一个线程准备向主存写数据的时候,第二个线程也立刻向主存写数据。volatile只能保证获取的是其他线程最后写入的值。想保证原子性可以用AtomicInteger,或者用synchronized关键字。
从指令级别说明Volatile,用volatile修饰的变量,转变为汇编代码后,回有lock前缀指令,在cpu中主要会发生两件事情。
1 将当前处理器缓存行的数据写回到系统内存。
2 其他CPU中读取该内存地址的数据将会设置为无效。
addl$0x0指令,让变量的修改立即写入到内存中,
addl$0x0指令在执行后,意味着之前的操作都已经完成,。
3 禁止指令重排序
内存屏障,保证特定操作的执行顺序,保证某些变量的内存可见性。
由于编译器和处理器会进行指令的优化,在指令的间隙插入内存屏障,禁止编译器和处理器在屏障的前后执行重排序优化,另外强制清空CPU中的缓存数据,各个CPU需要重主内存中获取数据,即volatile保证的可见性。
如果为volatile写,则在变量的前面添加storestore屏障,禁止前面的普通写和后面volatile写重排序,在后面添加storeloan屏障,防止后面的volatile变量读写与当前变量发生重排序
如果为volatile读,则在变量的后面添加loadloan屏障,禁止后面的普通变量读与当前变量发送重排序,添加loadstore屏障,保证后面的普通变量的写与当前变量发生重排序。