一、 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 …”);

  1. },"t1").start();
  2. try {
  3. Thread.sleep(1000);
  4. } catch (InterruptedException e) {
  5. throw new RuntimeException(e);
  6. }
  7. flag=false;
  8. }

}

  1. <a name="LKAuw"></a>
  2. #### 以上代码运行时是无法退出的,这是为啥呢?
  3. - 初始状态,t1线程刚开始从主内存中读取的flag的值到自己的工作内存
  4. ![](https://cdn.nlark.com/yuque/0/2022/jpeg/28810082/1653875125843-562d56ec-8d28-4059-bd4b-7b55e723c2da.jpeg)
  5. - 因为t1线程要频繁从主内存读取flag,JIT编译器会将flag值缓存到自己的工作内存中的告诉缓存中,下次就直接读取缓存中flag的值,减少对主内存的访问。
  6. ![](https://cdn.nlark.com/yuque/0/2022/jpeg/28810082/1653875646252-9f5e9ccb-ac6f-4e68-83c2-e4d602b25e60.jpeg)
  7. - 然后主线程修改了flag的值,并将flag的值同步到主存中,但是t1线程读取的是自己的高速缓存,不会去读主存中最新的值。
  8. ![](https://cdn.nlark.com/yuque/0/2022/jpeg/28810082/1653875862194-a57a6b00-8dcf-42b6-ad74-9abf2c932cb2.jpeg)
  9. <a name="ZP4Pl"></a>
  10. ### 2. 解决方法
  11. - 使用volatile关键字修饰**成员变量**和**静态成员变量**(放在主存中的变量),他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是**直接操作主存**
  12. ```java
  13. package panw.memory;
  14. import lombok.extern.slf4j.Slf4j;
  15. @Slf4j
  16. public class Test {
  17. static volatile Boolean flag = true;
  18. public static void main(String[] args) {
  19. new Thread(()->{
  20. while (flag){
  21. }
  22. log.debug("t1 end ...");
  23. },"t1").start();
  24. try {
  25. Thread.sleep(1000);
  26. } catch (InterruptedException e) {
  27. throw new RuntimeException(e);
  28. }
  29. flag=false;
  30. }
  31. }

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 {

  1. Monitor monitor = new Monitor();
  2. monitor.start();
  3. TimeUnit.SECONDS.sleep(4);
  4. monitor.stop();
  5. }

} @Slf4j class Monitor{ private Thread monitor; private boolean stop = false;

  1. public void start(){
  2. monitor = new Thread(()->{
  3. while (true){
  4. if (stop){
  5. log.debug("stop = true, 善后");
  6. break;
  7. }
  8. log.debug("继续运行");
  9. try {
  10. TimeUnit.SECONDS.sleep(1);
  11. } catch (InterruptedException e) {
  12. log.debug("被打断了");
  13. }
  14. }
  15. });
  16. monitor.start();
  17. }
  18. public void stop(){
  19. monitor.interrupt();
  20. stop = true;
  21. }

}

  1. <a name="jmDIx"></a>
  2. ### 5.同步模式之犹豫模式
  3. Balking (犹豫)模式用在一个线程发现另一个线程或本线程**已经做了某一件相同**的事,那么本线程就无需再做 了,**直接结束返回**
  4. - 用一个标记来判断该任务是否已经被执行过了
  5. - 需要避免线程安全问题
  6. ```java
  7. package panw.model;
  8. import lombok.extern.slf4j.Slf4j;
  9. import java.util.concurrent.TimeUnit;
  10. public class Test {
  11. public static void main(String[] args) throws InterruptedException {
  12. Monitor monitor = new Monitor();
  13. monitor.start();
  14. monitor.start();
  15. monitor.start();
  16. TimeUnit.SECONDS.sleep(4);
  17. monitor.stop();
  18. }
  19. }
  20. @Slf4j
  21. class Monitor{
  22. private Thread monitor;
  23. private boolean stop = false;
  24. private boolean starting = false;
  25. public void start(){
  26. synchronized (this){
  27. if (starting){
  28. log.debug("已经启动了,无需再次启动");
  29. return;
  30. }
  31. starting=true;
  32. }
  33. monitor = new Thread(()->{
  34. while (true){
  35. if (stop){
  36. log.debug("stop = true, 善后");
  37. break;
  38. }
  39. log.debug("继续运行");
  40. try {
  41. TimeUnit.SECONDS.sleep(1);
  42. } catch (InterruptedException e) {
  43. log.debug("被打断了");
  44. }
  45. }
  46. });
  47. monitor.start();
  48. }
  49. public void stop(){
  50. monitor.interrupt();
  51. stop = true;
  52. }
  53. }

三、有序性

1.指令重排

指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。编译器、处理器也遵循这样一个目标。注意是单线程。多线程的情况下指令重排序就带来问题。

2.数据依赖

主要指不同的程序指令之间的顺序是不允许进行交互的,即可称这些程序指令之间存在数据依赖性。
有以下例子:

写后读 a = 1; b = a; 写一个变量后,再读
写后写 a = 1; a = 2; 写一个变量后,再写
读后写 a = b; b =1; 读一个变量后,再写

这里每组指令中都有写操作,这个写操作的位置是不允许变化的,否则将带来不一样的执行结果。
编译器将不会对存在数据依赖性的程序指令进行重排,这里的依赖性仅仅指单线程情况下的数据依赖性;多线程并发情况下,此规则将失效。

3.指令重排带来的问题

单例模式失效

来看一个经典的懒汉式双重校验单例模式:

  1. public class Singleton {
  2. private static Singleton instance = null;
  3. private Singleton() { }
  4. public static Singleton getInstance() {
  5. if(instance == null) {
  6. synchronzied(Singleton.class) {
  7. if(instance == null) {
  8. instance = new Singleton(); //非原子操作
  9. }
  10. }
  11. }
  12. return instance;
  13. }
  14. }

instance = new Singleton();这一句其实并不是一个原子操作,可以抽象为:

  1. memory =allocate(); //1:分配对象的内存空间
  2. ctorInstance(memory); //2:初始化对象
  3. instance =memory; //3:设置instance指向刚分配的内存地址

上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:

  1. memory =allocate(); //1:分配对象的内存空间
  2. instance =memory; //3:设置instance指向刚分配的内存地址
  3. ctorInstance(memory); //2:初始化对象

可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。
在线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值给instance引用,恰好另一个线程进入方法判断instance引用不为null,然后就将其返回使用,导致出错。

解决办法:

上面提到的volatile既可以保证线程之间的可见性,又可以禁止指令重排,禁止的是加volatile关键字变量之前的代码被重排序。

4.内存屏障

  • 可见性
    • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
    • 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中新数据
  • 有序性

    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

      5.volatile原理

      volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
  • 对 volatile 变量的写指令后会加入写屏障

  • 对 volatile 变量的读指令前会加入读屏障

但是不能解决指令交错问题

  • 写屏障仅仅是保证之后的读能够读到新的结果,但不能保证读跑到它前面去
  • 而有序性的保证也只是保证了本线程内相关代码不被重排序