可见性、原子性、有序性问题是并发编程中的 BUG 源头,是编程领域的共性问题,可见性和原子性的产生原因是缓存和指令重排,Java 语言提供了内存模型来规避此类问题
Java 内存模型是一个复杂的规范,基本的原理就是禁用缓存和指令重排,提供给开发者的手段就是volatile
、synchronized
和 fina
l 三个关键字,以及六项Happens-Before
规则
volatile 关键字在许多语言中都有用到,原始含义就是禁用 CPU 缓存
volatile 解决可见性问题
public class App {
volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
//
}
System.out.println("end");
}).start();
TimeUnit.SECONDS.sleep(5);
flag = false;
}
}
主线程修改 flag,子线程内的死循环是否会结束?
如果不使用 volatile 声明,子线程一直会读取缓存内的值,而看不见其他线程的修改
使用 volatile 关键字,强制线程通过内存对该变量进行读写,而不是 CPU 缓存
volatile 解决有序性问题
在 JDK1.5 之前,volatile 被设计用来禁用缓存,但是并不关心指令的排序问题
在 JDK1.5 对 volatile 进行了增强,具体内容就是增加了一项 Happens-Before 规则
Happens-Before 规则是 Java 内存模型规范的重要部分
规则要表达的核心内容是在特定场景下,前面一个操作的结果对后续操作是可见的
程序的顺序性规则
在单线程中对一个变量的操作对后续的操作是可见的
volatile 变量规则
对一个 volatile 变量的写操作对后续的读操作是可见的(多线程的顺序性规则)
传递性规则
即 A 操作对 B 操作可见,B 操作对 操作可见,那么 A 操作对 C 操作可见
管程中锁的规则
一个线程的解锁操作对下一个线程加锁操作是可见的,如使用 synchronized
线程 start 规则
主线程调用子线程的 start 方法对子线程中的操作是可见的
线程 join 规则
子线程中的操作对主线程 join 方法是可见的
重排序可能导致一个线程看到一个对象的时候,这个对象还没有初始化完毕
部分初始化或者完全没有经过初始化,因为普通变量的写入是可以被重排序到构造器之外的
Java 内存模型要求编译器对 final 关键字特殊处理
即禁止处理器将 final 变量初始化操作重排序到构造器之外