上一章讲解的 Monitor 主要关注的是 访问共享变量时, 保护临界区代码的原子性
这一章节进一步深入学习 共享变量在多线程之间的【可见性】问题和 多条指令执行时的【有序性】问题
JMM 即 Java Memory Model, 它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM 体现在一下几个方面
主存 —— 所有共享信息存储的位置。
工作内存 —— 所有线程私有信息 存储的位置。
main线程 对 run变量的修改对于 t线程 不可见, 导致了 t 线程 无法停止:
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
// 。。。
}
});
t.start();
Thread.sleep(10);
run = false; // 线程 t 不会如设预想的停下来
}
分析一下:
volatile
synchronized
注意:
如果在上面例子 while中添加 System.out.println() 会发现即使不加 volatile 也能停下来
因为 源码实现 println 加了 synchronized锁
上面的例子中,如果连续启动两次start方法,那么就会创建两个监控线程,做相同的事,所以可以使用 Balking模式 改进一下。
这种特性 称为 【指令重排】,多线程下【指令重排】会影响正确性。
在不改变程序结果的前提下,这些指令的各个阶段可以通过 重排序 和 组合 来实现 指令级并行
分阶段,分工是提升效率的关键
指令重排的前提是,不能影响运行结果
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
I_Result 是一个对象,有一个属性 r1 用来保存结果, 问, 可能的结果有几种?
将ready 设置成 volatile ,就能避免volatile变量前面的代码被进行重排序
涉及到一个 写屏障。
volatile 的底层实现原理是内存屏障, Memory Barrier (Memory Fence)
public void actor2 (I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值 带写屏障
// 写屏障
}
public void actor1(I_Result r) {
// 读屏障
//ready 是 volatile 读取值带读屏障
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
public void actor2 (I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值 带写屏障
// 写屏障
}
public void actor1(I_Result r) {
// 读屏障
//ready 是 volatile 读取值带读屏障
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
volatile 只能保证 有序性 和 可见性,但是不能保证原子性,不能解决指令交错。
原子性:
synchronized 底层字节码是通过monitorenter的指令来进行加锁的、通过monitorexit指令来释放锁的。
可见性:
有序性:
如果 共享变量完全被synchronized 完全包裹起来,一般是不会出现问题
只有 共享变量 一部分暴露出来了,在多线程情况下,是不能控制其他线程访问不被包裹的部分,在加上同步块的指令重排序,可能出现问题。
写写屏障: 禁止写写屏障的前后写操作重排
读读屏障: 禁止读读屏障的前后读操作进行重排
读写屏障: 禁止读写屏障的前面的读操作进行重排 、 禁止读写屏障的后面的写操作重排
写读屏障:禁止写读屏障前面的写操作重排、禁止写读屏障后面的读写操作重排
著名的 double-checked locking 单例模式为例
用到时候再创建,只会创建一个。
但是锁范围太大,继续优化,两次判断,这也就是double-checked locking
以上实现的特点是:
但是在多线程环境下,上面的代码是有问题的。getInstance方法对应的字节码为:
指令重排的流程图
添加volatile的流程图
将 INSTANCE 变量设置成 volatile ,那么就会在变量后一个写屏障,保证 volatile 前面的代码不被重排序道后面去
那么底层的 调用构造方法 跟 赋值操作 的字节码操作就不会被其他线程中途打断,从而造成问题。
这时候就算是其他线程在赋值操作之前进行了调用,但是这时候进来获取到的是null,所以就需要在 synchronized 等待,所以能够保证安全性。