四、共享模型之内存
1、JAVA内存模型(JMM)
JMM 即 Java Memory Model,它定义了**主存(共享内存)、工作内存(线程私有)**抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。
JMM体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
2、可见性
引例
退出不出的循环
static Boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (run) {
//如果run为真,则一直执行
}
}).start();
Thread.sleep(1000);
System.out.println("改变run的值为false");
run = false;
}```java
**为什么无法退出该循环**
- 初始状态, t 线程刚开始从**主内存**读取了 run 的值到**工作内存**。
[![img](https://nyimapicture.oss-cn-beijing.aliyuncs.com/img/20200608145505.png)](https://nyimapicture.oss-cn-beijing.aliyuncs.com/img/20200608145505.png)
- 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值**缓存至自己工作内存**中的高速缓存中, 减少对主存中 run 的访问,提高效率
[![img](https://nyimapicture.oss-cn-beijing.aliyuncs.com/img/20200608145517.png)](https://nyimapicture.oss-cn-beijing.aliyuncs.com/img/20200608145517.png)
- 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量 的值,结果永远是**旧值**
[![img](https://nyimapicture.oss-cn-beijing.aliyuncs.com/img/20200608145529.png)](https://nyimapicture.oss-cn-beijing.aliyuncs.com/img/20200608145529.png)
**解决方法**
- 使用**volatile**易变关键字
- 它可以用来修饰**成员变量**和**静态成员变量**(放在主存中的变量),他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是**直接操作主存**
```java
//使用易变关键字
volatile static Boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (run) {
//如果run为真,则一直执行
}
}).start();
Thread.sleep(1000);
System.out.println("改变run的值为false");
run = false;
}
可见性与原子性
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况
-
注意 synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。
-
但缺点是 synchronized 是属于重量级操作,性能相对更低。
-
如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到 对 run 变量的修改了,想一想为什么?
-
因为使用了synchronized关键字
public void println(String x) { //使用了synchronized关键字 synchronized (this) { print(x); newLine(); } }
-
两阶终止模式优化
public class Test7 {
public static void main(String[] args) throws InterruptedException {
Monitor monitor = new Monitor();
monitor.start();
Thread.sleep(3500);
monitor.stop();
}
}
class Monitor {
Thread monitor;
//设置标记,用于判断是否被终止了
private volatile boolean stop = false;
/**
* 启动监控器线程
*/
public void start() {
//设置线控器线程,用于监控线程状态
monitor = new Thread() {
@Override
public void run() {
//开始不停的监控
while (true) {
if(stop) {
System.out.println("处理后续任务");
break;
}
System.out.println("监控器运行中...");
try {
//线程休眠
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("被打断了");
}
}
}
};
monitor.start();
}
/**
* 用于停止监控器线程
*/
public void stop() {
//打断线程
monitor.interrupt();
//修改标记
stop = true;
}
}
同步模式之犹豫模式
定义
Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做 了,直接结束返回
- 用一个标记来判断该任务是否已经被执行过了
- 需要避免线程安全问题
- 加锁的代码块要尽量的小,以保证性能
package com.nyima.day1;
/**
* @author Chen Panwen
* @data 2020/3/26 16:11
*/
public class Test7 {
public static void main(String[] args) throws InterruptedException {
Monitor monitor = new Monitor();
monitor.start();
monitor.start();
Thread.sleep(3500);
monitor.stop();
}
}
class Monitor {
Thread monitor;
//设置标记,用于判断是否被终止了
private volatile boolean stop = false;
//设置标记,用于判断是否已经启动过了
private boolean starting = false;
/**
* 启动监控器线程
*/
public void start() {
//上锁,避免多线程运行时出现线程安全问题
synchronized (this) {
if (starting) {
//已被启动,直接返回
return;
}
//启动监视器,改变标记
starting = true;
}
//设置线控器线程,用于监控线程状态
monitor = new Thread() {
@Override
public void run() {
//开始不停的监控
while (true) {
if(stop) {
System.out.println("处理后续任务");
break;
}
System.out.println("监控器运行中...");
try {
//线程休眠
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("被打断了");
}
}
}
};
monitor.start();
}
/**
* 用于停止监控器线程
*/
public void stop() {
//打断线程
monitor.interrupt();
stop = true;
}
}
3、有序性
指令重排
- JVM 会在不影响正确性的前提下,可以调整语句的执行顺序
这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。
指令重排序优化
- 事实上,现代处理器会设计为一个时钟周期完成一条执行时间长的 CPU 指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这5 个阶段
-
在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行
-
指令重排的前提是,重排指令不能影响结果,例如
// 可以重排的例子 int a = 10; int b = 20; System.out.println( a + b ); // 不能重排的例子 int a = 10; int b = a - 5;Copy
支持流水线的处理器
现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一 条执行时间长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。
在多线程环境下,指令重排序可能导致出现意料之外的结果
解决办法
volatile 修饰的变量,可以禁用指令重排
- 禁止的是加volatile关键字变量之前的代码被重排序
4、内存屏障
- 可见性
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
- 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中新数据
- 有序性
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
5、volatile 原理
volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
如何保证可见性
如何保证有序性
但是不能解决指令交错问题
- 写屏障仅仅是保证之后的读能够读到新的结果,但不能保证读跑到它前面去
- 而有序性的保证也只是保证了本线程内相关代码不被重排序
实现原理之Lock前缀
在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时
instance = new Singleton();
对应的汇编代码是
... lock addl ...
有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,通过查IA-32架构软件开发者手册可知,Lock前缀的指令在多核处理器下会引发了两件事
-
Lock前缀指令会引起处理器
缓存回写到内存
- Lock前缀指令导致在执行指令期间,声言处理器的LOCK#信号。在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存。但是,在最近的处理器里,LOCK #信号一般不锁总线,而是锁缓存,毕竟锁总线开销的比较大。使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据
-
一个处理器的缓存回写到内存会
导致其他处理器的缓存无效
- 在多核处理器系统中进行操作的时候,IA-32和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致