JVM volatile 错误理解和正确理解
可见性错误理解
public class TestVolatile extends Thread {
private static boolean test = true;
public static void main(String[] args) throws Exception {
new TestVolatile().start(); // 开始执行子线程的循环
Thread.sleep(100); // 保证循环代码运行次数足够多,触发JIT编译
test = false;
}
public void run() {
while (test) { }; // 死循环
}
}
错误理解: 因为while(test)代码块中没有代码或没有用到test引发Unreferenced,所以编译器将它编译成了while(true),不然为什么加入System.out.println(test);后就跳出循环了,肯定是用到了这个变量。
打脸操作:来看字节码被编译成什么了
public class cn.test.TestVolatile extends java.lang.Thread {
public cn.test.TestVolatile();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Thread."<init>" : ()V
4: return
public static void main(java.lang.String[]) throws java.lang.Exception;
Code:
0: new #2 // class cn/test/TestVolatile
3: dup
4: invokespecial #3 // Method “<init>” : ()V
7: invokevirtual #4 // Method start:()V
10: ldc2_w #5 // long 100l
13: invokestatic #7 // Method java/lang/Thread.sleep:(J)V
16: iconst_0
17: putstatic #8 // Field test:Z
20: return
public void run();
Code:
0: getstatic #8 // Field test:Z
3: ifeq 9
6: goto 0
9: return
static {};
Code:
0: iconst_1
1: putstatic #8 // Field test:Z
4: return
}
Process finished with exit code 0
可以看见是getstatic #8 然后是ifeq的话就到9(return),明显没有被编译成true,打不打脸?
不加volatile也可以跳出循环,不是应该不可见吗?
public class TestVolatile extends Thread {
private static boolean test = true;
public static void main(String[] args) throws Exception {
new TestVolatile().start(); // 开始执行子线程的循环
Thread.sleep(100); // 保证循环代码运行次数足够多,触发JIT编译
test = false;
}
public void run() {
while (test) {
System.out.println("随便写点东西");
} // 可以跳出循环
}
}
为什么可以跳出循环?难道是真的加了代码就一定能跳出循环?加了代码就变成可见的了?
我们将代码块中的代码换一个
public class TestVolatile extends Thread {
private static boolean test = true;
public static void main(String[] args) throws Exception {
new TestVolatile().start(); // 开始执行子线程的循环
Thread.sleep(100); // 保证循环代码运行次数足够多,触发JIT编译
test = false;
}
public void run() {
while (test) {
boolean flag = test;
} // 又成了死循环
}
}
代码块中加了代码啊,而且也用到了test?怎么这次又不跳出循环了?
原因
JVM会尽力保证内存的可见性,即便这个变量没有加volatile关键字。怎么尽力保证呢?
1.在同步方法结束后变量就会被刷回主存
2.用户手动让线程发生重新调度也会让变量变得可见。
3.调用任何一个volatile的读取或写入
至于为什么代码块中的 boolean flag = test; 不能跳出循环,而System.out可以跳出循环的原因在于:
你们用 java 多线程的时候没有发现System.out.println 并不会让输出混乱吗?其他语言这么做的话输出是混乱的,输出可能会变成"东随随西便写点写东西随写",而java并没有同时往输出缓冲区写东西。System.out.println是线程安全的,内部有synchronized,不信你加个 Thread.sleep(0);或Thread.yield();或调用个synchronized方法。
public class TestVolatile extends Thread {
private static boolean test = true;
public static void main(String[] args) throws Exception {
new TestVolatile().start(); // 开始执行子线程的循环
Thread.sleep(100); // 保证循环代码运行次数足够多,触发JIT编译
test = false;
}
//你可以去掉这个synchronized 测试一下
public synchronized void sync(){}
public void run() {
while (test) {
sync();
// synchronized(this){}
// synchronized(TestVolatile.class){}
// Thread.currentThread().getName(); //name是volatile的
// Thread.currentThread().getPriority();//这个就不行,因为priority没有volatile
}
}
}
有兴趣的可以了解一下happends before和四种内存屏障
事实上 如果在多线程状态下,如果某个成员变量同时涉及到了读和写,我们会加入同步代码块或锁之类的方式,所以volatile主要用在下面这类问题上。
DCL 单例模式一定要用到volatile吗
public class Singleton {
private volatile static Singleton instance = null;
public static Singleton getInstance() {
if(null == instance) {
synchronized (Singleton.class) {
if(null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
错误理解: 因为不加volatile会导致发生指令重排,可能先进行了分配空间,然后instance指向了这块空间,但是其中没有任何实例信息,所以用的时候会出错。
我的理解: 事实上并不是这样,一个对象的初始化在字节码中长这样:
new 创建指定类型的对象实例、对其进行默认初始化,并且将指向该实例的一个引用压入操作数栈顶
dup 拷贝一份引用 因为下面的初始化会消耗掉操作数栈顶的引用
invokespecial init 初始化对象
astore1 存储到本地变量
return
也许astore会被重排,其他线程只是判断是否存在instance,所以第一个线程只要触发new\dup\astore时,第二个线程刚好进来直接拿去用就会导致成员变量还未初始化,所以加volatile的原因在于,防止使用还没有进行初始化的对象,意思就是防止使用还没有执行构造函数的对象,如果你的构造函数中没有对成员的赋值操作,你也没有父类,事实上可以不加volatile。
永远不要过分相信别人说的话,自己做实验,人云亦云如果人家说的本身就是错的呢?