目录

  • 一、是什么
  • 二、为什么要有内存模型?
  • 三、内存交互的八个原子操作
  • 四、操作规则
  • 五、内存可见性
  • 六、解决内存可见性问题


一、是什么

Java多线程内存模型是基于Cpu缓存模型建立的,它的作用是屏蔽掉不同硬件和操作系统的内存访问差异,实现各种平台具有一致的并发效果。

java 多线程执行内存溢出问题 java多线程内存模型_java

二、为什么要有内存模型?

画一个简单的CPU缓存模型。

java 多线程执行内存溢出问题 java多线程内存模型_java_02


开始CPU是直接和主存进行交互的,但这样会有一个很大的问题,CPU的计算速度非常快,而主存是硬盘操作,比起CPU会慢很多很多,有时候CPU需要等待主存,导致效率很低。所以在CPU和主存之间加一个高速缓存作为缓冲,虽然高速缓存和CPU之间还存在速度差别,但比直接访问主存的效率高

的多。

注:这里的高速缓存是分成多级缓存,这里只是了解,简单画了一下。

三、内存交互的八个原子操作

线程从主内存中读取一个变量到自己的工作内存,线程结束时再把这个变量写回主内存中,是需要经过以下8个操作。(按顺序)

  1. lock(锁定):将一个变量标识成线程独有状态。
  2. read(读取):将变量的值读取到线程的工作内存中。
  3. load(加载):将读取到的值指向工作内存中变量副本。
  4. use(使用):把工作内存中的一个变量值传递给执行引擎。
  5. assign(赋值):将执行引擎收到的值赋值给变量副本。
  6. store(存储):将变量副本的值传递到主内存。
  7. write(写入):将值赋值给主内存的变量。
  8. unlock(解锁):将锁定的变量解锁。

这些操作需要按顺序执行,可以不连续执行,比如read和load之间可以夹杂其他操作。lock可选。

java 多线程执行内存溢出问题 java多线程内存模型_java_03

四、操作规则

  • 如果一个变量在线程中改变了,就必须同步到主内存中。
  • 如果一个变量没有发生过任何assign操作,就不需要往主内存写了。
  • 不运行read、load和store、wirte单独出现。
  • 一个变量只能同时被一个线程锁定,这个线程可以进行多次lock锁定,但必需对应次数的unlock解锁。
  • 对一个变量执行lock操作,那将会清空工作内存中此变量的值,需要重新load或assign。
  • unlock前必需store和write。
  • 不运行unlock未被lock或者被其他线程lock的变量。
  • 这些操作需要按顺序执行,可以不连续执行。

五、内存可见性

在内存模型中,线程会把内存中的变量a的值读取到自己的工作内存中,并赋值给工作内存中的副本变量,使用完之后再把新值写会内存,更新内存中变量a的值。

这样就会导致一个问题,看下面代码:

public class Demo1 {

    private static int a = 1;

    public static void main(String[] args) {

        new Thread(() -> {

            while (a == 1) {}

            System.out.println("a不等于1了....");
        }, "线程2").start();


       new Thread(() -> a = 2, "线程1").start();
    }
}

代码比较简单,一个线程判断a的值,一个线程改变a的值,思考一下,会不会执行 System.out.println("a不等于1了....");

答案:不会打印。看下图

java 多线程执行内存溢出问题 java多线程内存模型_jvm_04

线程1和线程2都用到了变量a,线程1将改变了a的值,并将a的值写回了主内存(不管怎样,线程结束之前肯定要写回),但线程1的这个操作对线程2来讲是不可见的,也就是线程2根本感知不到线程1修改了a的值,线程2中的a还是1,会一直while下去。这就是内存可见性。

这也会产生另一个问题,如果两个线程都是修改操作,就会导致一个线程会覆盖另一个线程刚修改好的值。这个问题在多线程数值计算时会造成错误的结果。

六、解决内存可见性问题

  1. 加锁

我们以synchronized锁为例,其他锁也可以。

在获取某个变量的锁的时候,会清空工作内存,重新将这个变量从主内存加载到工作内存中。

public class Demo1 {

    private static Integer a = 1;

    public static void main(String[] args) {

        new Thread(() -> {

            while (a == 1) {
                synchronized (a) {

                }
            }


            System.out.println("a不等于1了....");
        }, "线程2").start();


        new Thread(() -> a = 2, "线程1").start();
    }
}
  1. volatile关键字
    private static volatile int a = 1;,将变量用这个关键字修饰,就可以保证另一个线程修改这个值时对当前线程可见。
    简单说一下原理,jdk底层是c++实现的,底层代码中给变量赋值的时候,会判断这个变量是否是被volatile修饰的,如果是会在这个代码之后执行一个lock指令,这个指令不是内存屏障但有内存屏障的功能,它作用就是锁住这个变量的内存地址,并且立马将线程1中修改的值写回主存,线程2的cpu通过嗅探机制判断总线上是否有cpu要修改自己缓存中a的内存地址,如果有会将自己缓存中的值变成失效状态,下次在使用的时候,发现是失效状态的,会重新从主存中读取加载。