对于volatile的效果,大家可能都在书上或各种文章中见过,java语言规范里也对其有所描述。我对于它简单的理解就是多个线程(特别是多核情况下)共享访问修改一个实例或静态变量时,如果有一致性的需求可以通过将该变量声明为volatile,这样每次的读取都是强迫从主存读取,而非cpu的寄存器或cache.一次偶然的机会有个同事问起我怎么使用volatile,于是为了说明它的效果,我简单写了个程序:
/** * VolatileTest.java * * Copyright ekupeng,Inc. 2012 */ package test; /** * @ClassName: VolatileTest * @Description: Volatile测试 * @author Emerson emsn1026@gmail.com * @date 2012-11-29 下午06:57:44 * @version V1.0 * */ public class VolatileTest extends Thread { // 非volatile标志 private static boolean flag1 = false; // volatile标志 private static volatile boolean flag2 = false; private int i = 0; public void run() { //Object o = new Object(); //synchronized (o) { /* * 注释1 */ while (!flag1) { i++; //注意 : System.out.println(i); /* * 注释2 */ if (flag2) { System.out.println("over:" + i); break; } } //} } public static void main(String[] args) { VolatileTest t = new VolatileTest(); t.start(); try { Thread.currentThread().sleep(2000); // 先更改flag1 t.flag1 = true; /* * 注释3 */ Thread.currentThread().sleep(1000); // 将flag2置为true,如果有机会进入if(flag2),则将退出循环 t.flag2 = true; } catch (InterruptedException e) { e.printStackTrace(); } } }
因为预览时发现大段的注释会自动换行,影响阅读代码,所以我将代码中的注释提取出来:
注释1
外围标志flag1为非volatile,该线程(t)跑起来后由另一线程(main)将flag1改为true后,如果出现情况1.flag1如果不从主存重新读取,那他将继续以false运行,所以会继续循环并进入内部的flag2的if判断;如果出现情况2.flag1从主存重新读取,那他将以true运行,所以会跳出循环,也就没有机会进入flag2的if判断了;
注释2
如果出现情况1,将进入该判断,内部标志flag2为volatile,当线程(main)将flag2改为true后,因为flag2会从主存重新读取,将以true运行,所以将跳出循环,并打印"over"语句
注释3
为了确保flag1的变更有机会被t察觉,并保证flag2能在flag1变为true后进行一次以上while(!flag1)条件判断后再判断if(flag2),sleep1秒(1秒可以跑很多循环了)
以上是我为了说明volatile的功能写的一段程序,目的是想说一个线程1在循环中通过非volatile的布尔变量来进行条件判断,即使在另一个线程2中修改了该布尔变量,由于该线程1的代码执行得到了某种性能优化,不会从主存重新读取布尔值,导致进入死循环,直到内部的volatile布尔值被改变才跳出。
我的问题是:原本我以为会像预想那样的输出“over”语句,这样也说明了volatile的用处。但是我尝试了Sun JDK1.6,1.5,1.4,1.3,1.2(因为volatile是针对jit带来的优化,所以1.2之前的版本就没有尝试)之后发现只有1.2下才会看到该程序对于volatile的演示效果,输出了“over”语句。其他的都只是在外围的while(!flag1)中实时察觉flag1的变化并跳出了循环。原本以为是hotspot的问题,但是我尝试了hotspot的server或client,以及1.3的classic,都是没有效果的,只有1.2才能看到volatile的演示效果。哪位大神给细说下这个情况?
ps:Object那把锁没有实质的意义,只是进出synchronized块时会重新从主存同步数据,我当时随手写了测了下,所以大家可以不考虑,我暂且将它注掉吧....为了说明这一点,我加了行代码:
//注意 : System.out.println(i);
这行代码要是去掉注释,volatile在JDK1.2下也将失去作用,因为System.out.println中含有同步块,一执行该方法,变量将从主存中重新读取。
看到你的程序,我估计结果是什么都不输出,拿下代码,放入eclipse,{@fix:由于个人jdk配置,仅在jre1.6下运行},什么输出都没有。下面来说说原因:
1、对于非volatile修饰的变量,尽管jvm的优化,会导致变量的可见性问题,但这种可见性的问题也只是在短时间内高并发的情况下发生,CPU执行时会很快刷新Cache,一般的情况下很难出现,而且出现这种问题是不可预测的,与jvm, 机器配置环境等都有关。所以在未修改flag1之前,i会一直自增。一旦flag1修改后,sleep了1s,在flag2为修改之前,while循环就退出了,所以基本不会看到输出。
2、说说volatile的语义。volatile能保证可见性。其保证每次对volatile变量的读取会重新从主存中获取,以使得最新修改的值对其可见。(其大概的实现方式:每次写volatile变量时,会锁定系统总线,这样会导致其他CPU的Cache失效,这样下次读取时,CPU检测到Cache失效,会重新从主存中加载)。在jdk1.5之前,volatile只能保证可见性,但会re-order的问题,这也是著名的double-check-lock的问题(对此,可google出一大堆的文章)。在jdk1.5中,对volatile语义进行了增强,其保证jvm内存模型不会对volatile修饰的变量进行重排序(写volatile变量操作不会与其之前的读写操作重排,读volatile操作不会与其后的读写操作重排)[1], 之后double-check-lock才算实际的可用。
3、volatile提供的可见性和禁止指令重排的语义可以满足一定程度的同步性需求。对于volatile变量的使用,文献[2]中给出最佳实践:
参考文献:
[1] http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
[2] Java Concurrency in Practice. Author: Doug Lea. etc