「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战」
一、java内存模型
JMM(Java Memory Model),它定义了主存、工作内存的概念,底层同时对应着CPU的主存,缓存,寄存器,硬件等。
前面的文章简单提到过,如下图所示:
而JMM有以下几个重点:
- 原子性:线程上下文不会影响指令结果
- 可见性:CPU缓存不影响指令结果
- 有序性:CPU并行优化(指令重排序)不会影响指令结果
二、可见性
首先看如下的例子:
public class JMMTest {
static Boolean state = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(state){
}
},"t1").start();
TimeUnit.MILLISECONDS.sleep(1000);
state = false;
}
}
结果将导致t1线程无法停止,因为main方法修改成员变量state,对于线程t1是不可见的。
那么为什么会发生上述的问题?逐步分析一下:
- 主方法启动,静态变量state被加载到主内存当中,其值是true。
- 线程t1执行,从主内存加载state到线程私有内存(工作内存),值是true。
- 上述代码会不停的到主内存当中获取state值,所以JIT编译器(即时编译器)将state值缓存到自己的高速缓存当中。用以减少对主内存的访问。
- 当主线程在一秒后修改state为false时,修改的是主内存的值,而t1使用的是工作内存当中高速缓存中的值,所以仍然是true。
那么有什么解决方案呢?
使用volatile关键字。
volatile关键字可以修饰成员变量和静态变量。它能够使线程到主内存中获取值,避免去缓存当中取值。这就是可见性:
public class JMMTest {
static volatile Boolean state = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(state){
}
},"t1").start();
TimeUnit.MILLISECONDS.sleep(1000);
state = false;
}
}
注意:System.out.println将影响线程的可见性
如果代码如下所示:
public class JMMTest {
static Boolean state = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(state){
System.out.println("t1运行中");
}
},"t1").start();
TimeUnit.MILLISECONDS.sleep(1000);
state = false;
}
}
将不会出现我们预想的无法停止的情况。
原因,我们看println的源码发现,使用了synchronized关键字。
我们在前面学习synchronized的时候,学习过Monitor(监视器或管程),重量级锁需要使用Monitor去存储对象头的信息,同时其中的Owner会记录当前持有锁的线程。有且只能有一个线程持有锁,知道其退出,才可以有其他线程持有。
同步不仅仅表示互斥:这点在前面学习synchronized时没有介绍,其同样具有可见性的语义。
-
其他线程持有Monitor时:
- 获取Monitor,本地缓存失效,重新从主内存加载数据。
-
线程退出Monitor时:
- 释放Monitor,将线程私有内存(即缓存)刷新到主内存
结论:synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized 是属于重量级操作,性能相对更低。
经过以上简单解释,就能基本阐述这个问题的原因了。
三、原子性
这里不做过多解释,凡是涉及到多线程的地方都会有原子性的问题。
我们在前面文章学习的synchronized,就是通过互斥的线程同步方式,保证变量的原子性。
而在上一个小节提到的volatile关键字只能保证线程间的可见性,不能保证原子性。
除了synchronized之外,ReetrantLock、LockSupport等等可以保证原子性。
以及juc下面的很多类都是原子性的,后面的文章都会学到。
总而言之,java内存模型决定着,保证原子性是必要的也是必然的。
四、有序性
CPU(JVM)在不影响指令结果的前提下,会优化指令执行的顺序,但是在多线程的场景下,这有可能会造成问题,有些场景下我们是需要避免指令重排序的。
那么CPU为什么要做指令重排序优化呢?后面的文章会详解,本文只做入门理解。
如何禁止指令重排序?
答案同样是使用volatile,由此可见volatile的重要性
happens-before原则
关于有序性的内容,其实有一个很著名的原则:happens-before原则。
其规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见。
关于volatile的原理,在下一篇文章我会单独去讲解。