微信搜索 程序员的起飞之路 可以加我公众号,保证一有干货就更新~
二维码如下:
好,进入正题,今日学习 volatile 时,偶然想起之前见过的一段代码,正好说明了 volatile 的可见性,而我写博客也正好用的上。于是打算手撸一版出来,就有了下面的版本:
public class VolatileTest {
static class Test {
public volatile boolean flag = false;
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
new Thread(() -> {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.flag = true;
}).start();
while (true) {
if (test.flag) {
System.out.println("flag changed!");
break;
} else {
System.out.println("flag not changed!");
}
TimeUnit.SECONDS.sleep(1L);
}
}
}
然后我满心欢喜的执行了,希望出现如下效果:
但是!!!结果并不如我所愿!!竟然出现了这种结果:
WTF!!!我直接就爆了粗口。冷静下来仔细攻研代码,觉得没问题啊!一个线程读,另一个线程改。改的是工作副本中的数据,这里不应该能读到啊。这要是能读到我还要 volatile 干嘛!于是我默默的去百度别人家的代码,找到了如下代码:
public class VolatileDemo {
public static void main(String[] args) throws InterruptedException {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while (true) {
if (td.flag) {
System.out.println("flag changed");
break;
}
}
}
static class ThreadDemo implements Runnable {
boolean flag = false;
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2L);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag = " + flag);
}
}
}
这段代码跟我的没啥区别啊,只不过我新建的是对象,他新建的是个 Runnable 对象罢了,见鬼的是这个代码段执行结果是预期之中的 变量不可见导致循环未停止 现象!WTF???什么鬼!!我顿时傻眼,就拿这两段代码请教技术群的各位大佬。群里各位大佬出谋划策献计纷纷。最终我的代码被改成了如下:
public class VolatileTest {
static class Test {
public boolean flag = false;
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
new Thread(() -> {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.flag = true;
}).start();
while (!test.flag) {
}
System.out.println("flag changed!");
}
}
这样确实实现了我想要的效果:循环空转不停,flag changed! 未被打印。但是我还是不明白啊!到底我的代码哪里出了问题。这时一位名为 @颜如玉 的大佬跟我讲,System.out.println 中是有 synchronize 锁的,而 synchronize 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行 store、write 操作)。随后我在伟大的并发编程网上也找到了答案,文章是《同步和Java内存模型 (三)可见性》,答案如下:
一个写线程释放一个锁之后,另一个读线程随后获取了同一个锁。本质上,线程释放锁时会将强制刷新工作内存中的脏数据到主内存中,获取一个锁将强制线程装载(或重新装载)字段的值。锁提供对一个同步方法或块的互斥性执行,线程执行获取锁和释放锁时,所有对字段的访问的内存效果都是已定义的。
原文:A writing thread releases a synchronization lock and a reading thread subsequently acquires that same synchronization lock.
In essence, releasing a lock forces a flush of all writes from working memory employed by the thread, and acquiring a lock forces a (re)load of the values of accessible fields. While lock actions provide exclusion only for the operations performed within a synchronized method or block, these memory effects are defined to cover all fields used by the thread performing the action.
然后我在改完后的代码上加了一行 System.out.println(),虽然什么都没输出,但还是破坏了不可见现象。于是我返回我写的源代码,将 sout 删掉,改后代码如下:
public class VolatileTest {
static class Test {
public boolean flag = false;
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
new Thread(() -> {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.flag = true;
}).start();
while (true) {
if (test.flag) {
System.out.println("flag changed!");
break;
}
TimeUnit.SECONDS.sleep(1L);
}
}
}
然后开开心心的跑起来。嗯。不出你们所料。又特么失败了!!!又双叒叕!!我这次直接放弃挣扎了,果断甩代码进群。求大佬们帮助。结果当时在线的大佬们也是一筹莫展,表示一脸懵逼。那我就自己来,对比成功代码和失败代码以后发现,多了 TimeUnit.SECONDS.sleep(1L); 一句话。果断干掉,发现成了!!拿着结论去群里问大佬。有大佬就明白了,@我GTR就不服AE86 大佬说明了一下,sleep 0 触发了线程重新竞争cpu,线程要保存当前上下文才会释放cpu ,而保存上下文则将变量刷入了主内存,至此全部搞懂。改变 volatile 后也成功将两种现象复现。最终代码如下:
public class VolatileTest {
static class Test {
public boolean flag = false;
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
new Thread(() -> {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.flag = true;
}).start();
while (true) {
if (test.flag) {
System.out.println("flag changed!");
break;
}
}
}
}
读书越多越发现自己的无知,Keep Fighting!
欢迎友善交流,不喜勿喷~
Hope can help~