请教一个关于 volatile 数组的问题

2018-05-11 18:01:56 +08:00
 LittlePaper

在网络上搜索到的大部分的结论都是说 volatile 修饰的是数组的引用,不能保证数组元素的可见性,我写代码测试了一下:

public class VolatileTest {
    private volatile boolean[] running = {true};

    public void test() throws InterruptedException {
        new Thread(() -> {while (running[0]) {}}).start();
        Thread.sleep(1000);
        running[0] = false;
    }

    public static void main(String[] args) throws InterruptedException {
        new VolatileTest().test();
    }
}

上述代码运行是可以正常退出的,但如果去掉 volatile,则无法退出循环。这就与上述的结论矛盾了?

2939 次点击
所在节点    Java
12 条回复
momocraft
2018-05-11 19:06:07 +08:00
"不保证" 不是 "保证不"。试图用实验证明线程安全多少属于 cargo cult。
Luckyray
2018-05-11 19:07:09 +08:00
1 楼终结此贴
Luckyray
2018-05-11 19:08:23 +08:00
不对,我小看了 v2exer,坐等楼下大佬翻出来编译器的代码,解释下具体实现。
kiddult
2018-05-11 20:06:54 +08:00
加一下-XX:+PrintCompilation,你会发现 made not entrant 那行字在你设置 false 之前,直接被优化掉了
seaswalker
2018-05-11 20:08:50 +08:00
个人觉得这是提升优化,不加 volatile,编译器会优化成在 while 循环外判断一次,内部则是死循环
seaswalker
2018-05-11 20:46:09 +08:00
进一步说,这是 jit 编译器的提升优化,楼主可以试下下面的代码:
public class Test {

private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {

new Thread(new Runnable() {
@Override
public void run() {
while (flag);
System.out.println("退出");
}
}).start();

Thread.sleep(500);

flag = false;
}

}

在两种情况下可以退出,
1. flag 加 volatile
2. 加上 JVM 参数-Xint 关闭 JIT 编译。我觉着其实这里并没有什么可见性问题,这种单个变量的修改本身就应该是原子的,volatile 不可能加速其它 CPU 看到修改的过程,这里的 volatile 准确来说是对编译器的提示,告诉编译器这个变量是可能被修改的,不要随便搞事情。。。
Infernalzero
2018-05-11 21:16:48 +08:00
应该这样写
public class VolatileTest {
private volatile boolean[] running = { true };

public void test() throws InterruptedException {
new Thread(() -> {
final boolean a = running[0];
while (a) {
}
}).start();
Thread.sleep(1000);
running[0] = false;
}

public static void main(final String[] args) throws InterruptedException {
new VolatileTest().test();
}
}
LittlePaper
2018-05-11 21:37:18 +08:00
@seaswalker 谢谢,确实是 JIT 引起的。原来一直以为是可见性的问题,很多文章都这么写,这次想到数组元素的可见性应该是不受 volatile 影响的,没想到结果出乎意外。不过按我的理解与猜测,可见性的问题理论上是存在的,一个线程修改了共享变量的值,另外一个线程不能立即看到,但最终能够看到,例如会定期地根据主内存的内容刷新工作内存,可能依赖于具体实现。其实我之前也发现了不用 volatile 也不一定造成循环无法退出,例如若在循环中有打印语句的话也可以退出,看来只是在这种简单的空循环下,由于编译优化造成了死循环。
alamaya
2018-05-11 21:54:06 +08:00
volatile 两大功能,一个可见性,一个指令重排
LittlePaper
2018-05-11 22:49:07 +08:00
@Infernalzero 这里是原生类型( boolean ),a 是另外一个独立的变量,当然会死循环。
seaswalker
2018-05-11 23:10:29 +08:00
再补充几点。一个 CPU 在修改 cache line 之前首先要获得对其的排他控制权,即要向其它 CPU 发送使无效消息,而为了保证性能,每个 CPU 均有一个 Invalidate Queue 用于处理使无效消息,但是 CPU 不提供何时处理使无效消息的保证。Java 的 volatile 实现会在读时插入一个 smp_rmb(),但是 CPU 在遇到读屏障时不会马上刷新 Invalidate Queue,而是只保证顺序,这就是为什么我上面说 volatile 不会加速其它 CPU 看到修改。所以在单个变量的读写上,其实根本没必要使用 CPU 层面上的内存屏障,对付编译器的屏障足矣,这就是 Linux 内核 ACCESS_ONCE 宏的作用,然而 Java 却没得选。。。2333
seaswalker
2018-05-11 23:19:26 +08:00
可见性这个东西,我上面说的没有可见性问题,指的是硬件层面。我觉得 Java 里面的可见性指的是两个方面:

1. 软件层面,编译器重排。
2. 硬件层面上的多变量访问的顺序问题。

可能我们说的都没错,硬件上确实没有顺序问题,而由于 JIT 的优化确实产生了"不可见"的结果,一个概念的两个层面。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/454134

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX