在网络上搜索到的大部分的结论都是说 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,则无法退出循环。这就与上述的结论矛盾了?
1
momocraft 2018-05-11 19:06:07 +08:00
"不保证" 不是 "保证不"。试图用实验证明线程安全多少属于 cargo cult。
|
2
Luckyray 2018-05-11 19:07:09 +08:00 via iPhone
1 楼终结此贴
|
3
Luckyray 2018-05-11 19:08:23 +08:00 via iPhone
不对,我小看了 v2exer,坐等楼下大佬翻出来编译器的代码,解释下具体实现。
|
4
kiddult 2018-05-11 20:06:54 +08:00 1
加一下-XX:+PrintCompilation,你会发现 made not entrant 那行字在你设置 false 之前,直接被优化掉了
|
5
seaswalker 2018-05-11 20:08:50 +08:00 via iPhone
个人觉得这是提升优化,不加 volatile,编译器会优化成在 while 循环外判断一次,内部则是死循环
|
6
seaswalker 2018-05-11 20:46:09 +08:00 1
进一步说,这是 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 准确来说是对编译器的提示,告诉编译器这个变量是可能被修改的,不要随便搞事情。。。 |
7
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(); } } |
8
LittlePaper OP @seaswalker 谢谢,确实是 JIT 引起的。原来一直以为是可见性的问题,很多文章都这么写,这次想到数组元素的可见性应该是不受 volatile 影响的,没想到结果出乎意外。不过按我的理解与猜测,可见性的问题理论上是存在的,一个线程修改了共享变量的值,另外一个线程不能立即看到,但最终能够看到,例如会定期地根据主内存的内容刷新工作内存,可能依赖于具体实现。其实我之前也发现了不用 volatile 也不一定造成循环无法退出,例如若在循环中有打印语句的话也可以退出,看来只是在这种简单的空循环下,由于编译优化造成了死循环。
|
9
alamaya 2018-05-11 21:54:06 +08:00
volatile 两大功能,一个可见性,一个指令重排
|
10
LittlePaper OP @Infernalzero 这里是原生类型( boolean ),a 是另外一个独立的变量,当然会死循环。
|
11
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
|
12
seaswalker 2018-05-11 23:19:26 +08:00
可见性这个东西,我上面说的没有可见性问题,指的是硬件层面。我觉得 Java 里面的可见性指的是两个方面:
1. 软件层面,编译器重排。 2. 硬件层面上的多变量访问的顺序问题。 可能我们说的都没错,硬件上确实没有顺序问题,而由于 JIT 的优化确实产生了"不可见"的结果,一个概念的两个层面。 |