我被自己蠢哭了(〒︿〒)…
还是要戒骄戒躁, 踏踏实实地学习才行.
“一个人知道自己为何而活, 就可以忍受任何生活” - 尼采.
1 共享变量的内存可见性问题
看这个示例:
public class Test1 {
private static boolean b = false;
public static void main(String[] args) throws InterruptedException {
// 假设这个线程叫 T0
new Thread(() -> {
for (; ; ) {
if (b) {
System.out.println(
Thread.currentThread().getName() + " " + b);
break;
}
}
}).start();
Thread.sleep(300);
// 可以用下面的 while 循环代替 Thread.sleep(300),
// 它俩的消耗的时间差不多
// long i = 0;
// while (i++ < 1_000_000_000L) {
// }
System.out.println("b 的值为 : " + b);
b = true;
System.out.println("修改 b 的值为 : " + b);
}
}
结果:
程序会一直在 T0 线程
的死循环中, 无法退出. 而且 if (b)
条件无法满足.
尽管 main 线程
已经把 b
的值 修改为 true
, 但是 T0 线程
读到的还是 false.
解释:
JMM 规定:
所有的共享变量(实例变量和类变量)都存储于 主内存.
每一个线程还存在自己的 工作内存,线程的 工作内存, 保留了被线程使用的变量的工作副本.
线程对变量的所有读写操作 都必须在 工作内存 中完成, 而不能直接读写 主内存 中的变量.
主内存 和 工作内存 关系如下图(图片来源):
这就可以解释程序的执行结果了:
T0 线程
开始执行, 进入死循环, 第一次读取 b, 线程的 工作内存 没有, 就去 主内存 复制一份到自己的 工作内存 . 此时b = false
, 程序执行死循环,if 条件
一直不满足.main 线程
修改 共享变量b = true
后, 写回到 主内存 .T0 线程
再次读取 b, 因为 b 在 自己 工作内存 中有值(false), 所以T0 线程
不会去读取 主内存 的值, 所以if 条件
任然不满足.- 所以,
T0 线程
一直在运行死循环, 无法退出.
如果用 volatile 关键字
修饰 b:
private static volatile boolean b = false;
那么程序的执行结果将是:
b 的值为 : false
修改 b 的值为 : true
Thread-0 true
volatile
保证不同线程对 共享变量 操作的 可见性.
也就是说一个线程修改了 volatile
修饰的变量, 当修改写回 **主内存**时, 其他已经读取的线程的 变量副本 会 置为失效, 其他线程再次读数据时, 因 工作内存 的数据已经失效, 所以它会去 主内存 中读取, 并复制到自己的 工作内存 中.
题外话: volatile
不仅可以保证 内存可见性, 它还可以 禁止指令重排(单例模式-双重检查加锁实现就用到了 volatile
禁止指令重排 的特性).
2 避坑指南
稍微改一下上面的程序:
public class Test1 {
private static boolean b = false;
public static void main(String[] args) throws InterruptedException {
// 假设这个线程叫 T0
new Thread(() -> {
for (; ; ) {
if (b) {
System.out.println(
Thread.currentThread().getName() + " " + b);
break;
} else {
System.out.println("rosie..");
}
}
}).start();
Thread.sleep(300);
System.out.println("b 的值为 : " + b);
b = true;
System.out.println("修改 b 的值为 : " + b);
}
}
执行结果:
...
(以上省略无数个rosie..)
rosie..
b 的值为 : false
修改 b 的值为 : true
rosie..
Thread-0 true
卧槽! 什么情况?
明明没有用 volatile
修饰共享变量, T0 线程
竟然读到了 主内存 里 更新后的 共享变量.
这是因为 System.out.println 方法
中使用了 synchronized 关键字
.
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
synchronized 关键字
也可以保证 内存一致性:
一个线程进入 synchronized代码块
前, 线程会先获得 monitor 锁, 并 清空工作内存, 从 主内存 拷贝 共享变量 最新的值 到 工作内存 成为副本, 然后执行synchronized代码块
, 最后将修改后的副本的值刷新回 主内存 中, 线程释放锁.
3 探坑指南
稍微改一下上面的程序:
public class Test1 {
private static boolean b = false;
public static void main(String[] args) throws InterruptedException {
// 假设这个线程叫 T0
new Thread(() -> {
for (; ; ) {
if (b) {
System.out.println(
Thread.currentThread().getName() + " " + b);
break;
}
// 只是加了一个 sleep
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(300);
System.out.println("b 的值为 : " + b);
b = true;
System.out.println("修改 b 的值为 : " + b);
}
}
执行结果:
b 的值为 : false
修改 b 的值为 : true
Thread-0 true
可以看到, 没有用 volatile
修饰共享变量, T0 线程
也读到了 主内存 里 更新后的 共享变量.
可以猜测, Thread.sleep 方法
应该也会清除 工作内存, 重新从 主内存 中获取共享变量的值. (纯属猜测, 没有看到相关的文档有介绍)