玩java多线程的,大多都知道volatile:它能保证变量的可见性,其它线程能看到其最新值,但不能用于实现线程安全的变量自增;再深入点的可能知道,它会限制指令重排序,volatile操作前的操作(包括普通变量的读写)不能重排到它之后,反之亦然。

基于上面的认识,我设计了下面的测试

public class TestVolatile {
    private volatile int n1=0;//volatile
    private  int n2=0;

    public static void main(String[] a) {
        new TestVolatile().test();
        new TestVolatile().test();
    }

    public void test() {
        Thread t1 = new Thread() {
            public void run() {
                do{
                }while(n2-n1<=0);
                System.out.println("n2>n1");
            }
        };
        Thread t2 = new Thread() {
            public void run() {
                    for (;n1 < Integer.MAX_VALUE; ) {
                        ++n1;
                        ++n2;
                    }
                    System.out.println("stoped");
            }
        };
        t1.start();
        t2.start();
    }
}

对于n1,n2,只有t2线程对其修改,并且n1总是先于n2自增,所以有:

n1在任何时刻都大于或等于n2

然后在t1线程里,根据表达式的顺序,先读取n2,再读取n1,因为都在增大,所以后读取的应该比先读取的大,并且n1是volatile的,能保证读到的是最新值,所以应该有

在t1线程里,表达式n2-n1<=0恒成立

但事实却并非如此,此测试会输出“n2>n1”(注意:需要让JVM在server模式下进行测试)

此问题困扰了我几个月,百思不得其解,最近这几天,在一篇深入分析volatile的文章(本文末尾有链接)里找到了答案

其实volatile并不保证所有情况都不进行重排序,像下面两种情况,是允许指令重排序的

1、普通变量的读/写操作,然后volatile变量的读操作

2、volatile变量的写操作,然后普通变量的读/写操作

再回过来看上面的测试,

++n1;可以看作是temp=n1;temp=temp+1;n1=temp;最后执行的肯定是一个写入操作,接着是++n2;是普通操作,按照上面的规则2,此处允许重排序,n2的写入若被排到了n1的写入之前,那么n1>=n2就不是恒成立的了;

不仅t2线程这出了问题,t1线程里也出了问题

n2-n1<=0,先是读取普通变量n2,然后是volatile变量n1的读操作,按照规则1,也是可以重排序的,如果先读取了n1,接着t2线程让n1、n2都增加了,t1然后再读了n2,那么完全有可能n2大于n1

至此问题已经分析清楚了,volatile并不严格保证指令不被重排序。

既然如此,这算不算是volatile的设计缺陷呢?

其实这不能算缺陷,因为该测试中,对volatile的使用本身就是不合适的(没加volatile的变量对其它线程的可见性本身就有问题,其值是不确定的),没必为一个错误的用法作出严格的保证

若想详细了解volatile,请参考此文:http://www.infoq.com/cn/articles/java-memory-model-4

在此感谢作者 程晓明,解开了我几个月来的困惑。

posted on 2013-03-21 23:08  trytocatch  阅读(2203)  评论(0编辑  收藏  举报