1. Java 内存模型
JMM 即 Java Memory Model,它定义了主存(所有线程都共享的数据,包括成员变量)、工作内存(线程私有的数据,包括局部变量)抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面:
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
2. 原子性
Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性
3. 可见性
3.1 可见性问题
退不出的循环:
先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:
示例代码:
package tian;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test32")
public class Test32 {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
if (!run) {
break;
}
}
});
t.start();
run = false;
}
}
问题原因:
-
初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。 -
因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率 -
1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
3.2 可见性问题解决
3.2.1 解决方法1( 推荐 ): volatile(易变关键字)
volatile(易变关键字):
- 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。
- volatile 关键字可以保证共享变量在多个线程之间的可见性
3.2.2 解决方法2: synchronized
package tian;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test32")
public class Test32 {
static boolean run = true;
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true) {
synchronized (lock) {
if (!run) {
break;
}
}
}
});
t.start();
Thread.sleep(1000);
synchronized (lock) {
run = false;
}
}
}
3.2 可见性 VS 原子性
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况: 上例从字节码理解是这样的: 比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,只能保证看到最新值,不能解决指令交错
注意:
synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized 是属于重量级操作,性能相对更低。
|