今天看到黑马视频讲到的volatile的可见性,颠覆了我之前对volatile的认知
之前认为共享变量不加volatile是这样的
但是看了视频讲的和上面的并不完全相同
不加volatile
@Slf4j(topic = "d.VolatileForeverLoop")
public class VolatileForeverLoop {
static boolean stop = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
log.debug("stop to true");
},"t1").start();
foo();
}
private static void foo() {
int i = 0;
while (!stop) {
i++;
}
log.debug("stop 循环次数{}", i);
}
}
运行是无法停止的
线程t1 将stop= true
主线程 从主内存中读取false到本地内存 执行while()循环
主线程 是无法感知到stop=true的
但是再新加一个线程t2
@Slf4j(topic = "d.VolatileForeverLoop")
public class VolatileForeverLoop {
static boolean stop = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
log.debug("stop to true");
},"t1").start();
new Thread(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
log.debug("{}}",stop);
},"t2").start();
foo();
}
private static void foo() {
int i = 0;
while (!stop) {
i++;
}
log.debug("stop 循环次数{}", i);
}
}
可以看到t2线程能够读到t1将stop改为true的
再加一个t3也一样能够读到,只有主线程读不到true。
这里已经显示了并非t1没有将stop=true写回主内存,因为t2已经读到了,但是主线程没有读到
原因:
CPU-0读取物理内存的stop值为false,那么线程1的while条件满足进入下一次循环,一直读取false一直循环,这样while循环读取的次数是非常多的,
正常编译字节码使用的是解释器,
当循环到达一定次数 ,此时JIT编译器对代码进行了优化,对于热点的代码(频繁调用,反复执行)直接将false替换stop,并将stop进行备份
导致线程2给stop赋值改成true,写回主内存,线程也1无法感知到stop的变化
证明
VM配置参数-Xint
表示只用解释器进行编译,不用JIT优化
运行结果如下,此时主线程能够读到stop = true ,100millis循环了600W次
13:39:42 [t1] d.VolatileForeverLoop - stop to true
13:39:42 [main] d.VolatileForeverLoop - stop 循环次数6495969
13:39:42 [t2] d.VolatileForeverLoop - true}
还有一种情况,当t1线程执行的时间非常短,不够使JIT对代码进行优化
将VM配置去除,将t1睡眠时间改为1millis
@Slf4j(topic = "d.VolatileForeverLoop")
public class VolatileForeverLoop {
static boolean stop = false;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
log.debug("stop to true");
}, "t1").start();
new Thread(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
log.debug("{}}", stop);
}, "t2").start();
foo();
}
private static void foo() {
int i = 0;
while (!stop) {
i++;
}
log.debug("stop 循环次数{}", i);
}
}
运行结果
13:44:15 [t1] d.VolatileForeverLoop - stop to true
13:44:15 [main] d.VolatileForeverLoop - stop 循环次数82643
13:44:15 [t2] d.VolatileForeverLoop - true}
1millis循环8W次,循环次数过小,JIT认为不属于热点代码,并没有对代码进行优化,
主线程能够感知到stop的变化
第一 -Xint只是用解释器,禁用JIT编译器
第二修改变量执行的时间非常短只有1millis级别
这两种情况都可以使主线程感知到变量的变化
首先执行时间是不可控的,
其次禁用编译器会导致整体性能变低(JIT会有多级优化),JIT编译器能够提升数十倍的编译效率,不能因为一个变量而去降低整体的性能。
所以最终的解决方案还是使用volatile
而加入volatile 是为了告诉JIT不要对这个变量进行优化
从这个原因到证明已经很好地说明了是JIT搞得鬼,
之前了解的例子都是睡眠几百毫秒甚至几秒的,让我以为是修改值后不会立即从本地内存写回主内存
但是试过睡眠1毫秒甚至用睡眠,竟然能够读到修改的值 ,
这和没有加入volatile,修改值后不会立即从本地内存写回主内存,然后加了volatile就会修改后立即刷回主内存貌似没关系,
没有加入volatile应该也会从本地内存写回主内存(可能有所谓的不会立即会写),但是应该是很快的
我想知道各位的看法,如果有误的地方,请不吝赐教,谢过各位了