Java中volatile的疑问

对于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中含有同步块,一执行该方法,变量将从主存中重新读取。

阅读 14.5k
7 个回答

看到你的程序,我估计结果是什么都不输出,拿下代码,放入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

// 非volatile标志 这个很难说得准就一定拷贝之类的,你还是倒过来试试 用// volatile标志 。
或许还跟同步哪个对象有关?
或许又跟静态变量有关?
只是路过~

我理解的volatile并非用在你的例子所描述的场景里的。不考虑volatile存在的情况,如果修改flag1的值不能让程序跳出循环,恐怕也不是我们想要的结果。
volatile主要是为了用在数据读写的一致性问题上的。举个例子,在32位机上,一个long是64位,那么给一个long赋值就至少需要两条指令,先写高位再写低位。如果有两个线程,一个写,一个读,就有可能出现读的线程读到了只写了一半的值,出现不一致的情况。volatile保证了能读到完整的值。

想验证volatile比较难, 下面的实验在我的jre7上可以重现:

//1. 使用volatile, 程序可以退出.

public class VolatileTest extends Thread {
    private static volatile boolean flag = false;
    public void run() {while (!flag);}
    public static void main(String[] args) throws Exception {
        new VolatileTest().start();
        Thread.sleep(2000);
        flag = true;
    }
}

//2. 去掉volatile, 程序无法退出.

public class VolatileTest extends Thread {
    private static boolean flag = false;
    public void run() {while (!flag);}
    public static void main(String[] args) throws Exception {
        new VolatileTest().start();
        Thread.sleep(2000);
        flag = true;
    }
}

类似的例子

volatile的作用是使CPU在读取变量值时,一定得去主存读取,不能直接从缓存读取

循环肯定会终止啊,t.flag1 = true;
/*
* 注释3
/
Thread.currentThread().sleep(1000);
// 将flag2置为true,如果有机会进入if(flag2),则将退出循环
t.flag2 = true;
main线程,对volatile变量flag2的修改,会导致,共享变量flag1和flag2刷新到主内存。
当线程 while (!flag1) {
i++;
//注意 : System.out.println(i);
/

* 注释2
*/
if (flag2) {
System.out.println("over:" + i);
break;
}
对volatiel 变量flag2读的时候,线程会从主存读取数据,此时flag1 = true.
所以程序执行的效果,是循环一会后,退出外层循环。

部分关键词探究
当写一个volatile变量时,JMM会把所有线程本地内存的对应变量副本刷新回主存;(注意是所有共享变量,不是一个volatile变量)

你可以从这个方向找资料。

/**
 * 当写一个volatile变量时,JMM会把所有线程本地内存的对应变量副本刷新回主存;(注意是所有共享变量,不是一个volatile变量)
 */
public class VolatileTest extends Thread {

    private static boolean flag1 = false;
    private static volatile boolean flag2 = false;

    private int i = 0;

    public void run() {
        while (!flag1) {
            System.out.println("flag1:" + flag1 + "------------------- flag2:" + flag2);
        }
        System.out.println("------------------------------------------------------------------");
        System.out.println("flag1:" + flag1 + "------------------- flag2:" + flag2);
        System.out.println("------------------------------------------------------------------");
    }

    public static void main(String[] args) {
        VolatileTest t = new VolatileTest();
        t.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
//                    Thread.currentThread().sleep(2000);
                    flag1 = true;
                    System.out.println("flag1 changed");
                    Thread.currentThread().sleep(1000);
                    flag2 = true;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        t2.start();
    }
}

你可以把volatile去掉试试。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏