一、 JAVA内存模型(JMM)
JMM 即 Java Memory Model,它定义了主存(共享内存)、工作内存(线程私有)抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。
JMM体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
二、可见性
1. 引例
```java package panw.memory;
import lombok.extern.slf4j.Slf4j;
@Slf4j public class Test { static Boolean flag = true; public static void main(String[] args) { new Thread(()->{ while (flag){ } log.debug(“t1 end …”);
},"t1").start();try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}flag=false;}
}
<a name="LKAuw"></a>#### 以上代码运行时是无法退出的,这是为啥呢?- 初始状态,t1线程刚开始从主内存中读取的flag的值到自己的工作内存- 因为t1线程要频繁从主内存读取flag,JIT编译器会将flag值缓存到自己的工作内存中的告诉缓存中,下次就直接读取缓存中flag的值,减少对主内存的访问。- 然后主线程修改了flag的值,并将flag的值同步到主存中,但是t1线程读取的是自己的高速缓存,不会去读主存中最新的值。<a name="ZP4Pl"></a>### 2. 解决方法- 使用volatile关键字修饰**成员变量**和**静态成员变量**(放在主存中的变量),他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是**直接操作主存**```javapackage panw.memory;import lombok.extern.slf4j.Slf4j;@Slf4jpublic class Test {static volatile Boolean flag = true;public static void main(String[] args) {new Thread(()->{while (flag){}log.debug("t1 end ...");},"t1").start();try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}flag=false;}}
3. 可见性与原子性
注意volatile只可以保证线程之间的可见性,但是不可以保证操作原子性,只适用于一写多读的情况
synchronized
- synchronized 语句块既可以保证可见性也可以保证原子性。
- 但缺点是 synchronized 是属于重量级操作,性能相对更低。
4. 两阶段终止模式优化
```java package panw.model;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
public class Test { public static void main(String[] args) throws InterruptedException {
Monitor monitor = new Monitor();monitor.start();TimeUnit.SECONDS.sleep(4);monitor.stop();}
} @Slf4j class Monitor{ private Thread monitor; private boolean stop = false;
public void start(){monitor = new Thread(()->{while (true){if (stop){log.debug("stop = true, 善后");break;}log.debug("继续运行");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {log.debug("被打断了");}}});monitor.start();}public void stop(){monitor.interrupt();stop = true;}
}
<a name="jmDIx"></a>### 5.同步模式之犹豫模式Balking (犹豫)模式用在一个线程发现另一个线程或本线程**已经做了某一件相同**的事,那么本线程就无需再做 了,**直接结束返回**- 用一个标记来判断该任务是否已经被执行过了- 需要避免线程安全问题```javapackage panw.model;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;public class Test {public static void main(String[] args) throws InterruptedException {Monitor monitor = new Monitor();monitor.start();monitor.start();monitor.start();TimeUnit.SECONDS.sleep(4);monitor.stop();}}@Slf4jclass Monitor{private Thread monitor;private boolean stop = false;private boolean starting = false;public void start(){synchronized (this){if (starting){log.debug("已经启动了,无需再次启动");return;}starting=true;}monitor = new Thread(()->{while (true){if (stop){log.debug("stop = true, 善后");break;}log.debug("继续运行");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {log.debug("被打断了");}}});monitor.start();}public void stop(){monitor.interrupt();stop = true;}}
三、有序性
1.指令重排
指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。编译器、处理器也遵循这样一个目标。注意是单线程。多线程的情况下指令重排序就带来问题。
2.数据依赖
主要指不同的程序指令之间的顺序是不允许进行交互的,即可称这些程序指令之间存在数据依赖性。
有以下例子:
| 写后读 | a = 1; b = a; | 写一个变量后,再读 |
|---|---|---|
| 写后写 | a = 1; a = 2; | 写一个变量后,再写 |
| 读后写 | a = b; b =1; | 读一个变量后,再写 |
这里每组指令中都有写操作,这个写操作的位置是不允许变化的,否则将带来不一样的执行结果。
编译器将不会对存在数据依赖性的程序指令进行重排,这里的依赖性仅仅指单线程情况下的数据依赖性;多线程并发情况下,此规则将失效。
3.指令重排带来的问题
单例模式失效
来看一个经典的懒汉式双重校验单例模式:
public class Singleton {private static Singleton instance = null;private Singleton() { }public static Singleton getInstance() {if(instance == null) {synchronzied(Singleton.class) {if(instance == null) {instance = new Singleton(); //非原子操作}}}return instance;}}
instance = new Singleton();这一句其实并不是一个原子操作,可以抽象为:
memory =allocate(); //1:分配对象的内存空间ctorInstance(memory); //2:初始化对象instance =memory; //3:设置instance指向刚分配的内存地址
上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:
memory =allocate(); //1:分配对象的内存空间instance =memory; //3:设置instance指向刚分配的内存地址ctorInstance(memory); //2:初始化对象
可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。
在线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值给instance引用,恰好另一个线程进入方法判断instance引用不为null,然后就将其返回使用,导致出错。
解决办法:
上面提到的volatile既可以保证线程之间的可见性,又可以禁止指令重排,禁止的是加volatile关键字变量之前的代码被重排序。
4.内存屏障
- 可见性
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
- 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中新数据
有序性
对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
但是不能解决指令交错问题
- 写屏障仅仅是保证之后的读能够读到新的结果,但不能保证读跑到它前面去
- 而有序性的保证也只是保证了本线程内相关代码不被重排序
