Java内存模型(JMM)
即共享内存模型,JMM 决定一个线程对共享变量的写入时,能对另一个线程可见。
从抽象的角度来看,JMM 定义了线程和主内存之间的抽象关系:
- 主内存(main memory):存储线程之间的共享变量,
- 本地内存(local memory):每个线程私有的,本地存储中存储了该线程以读/写共享变量的副本。
本地内存是 JMM 的一个抽象概念,并非真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
JMM 中的原子操作
在JMM中,定义了8个原子操作来实现一个共享变量如何从主内存拷贝到工作内存,以及如何从工作内存同步到主内存,交互如下
8个原子操作指令
-
lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
-
unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
-
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
-
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
-
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
-
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
-
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
-
write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
JVM 对 JMM 内存模型的实现
在JVM内部,Java内存模型把内存分成了两部分:
- 线程栈区
- 堆区
JVM中运行的每个线程都拥有自己的线程栈,线程栈包含了当前线程执行的方法调用相关信息,我们也把它称作调用栈。随着代码的不断执行,调用栈会不断变化。
基于JMM(内存模型)分析非线程安全
由JMM的原子操作中可以看出,每一个线程中的本地私有内存都是独立的,所有过的线程在未加锁的情况下操作全局变量步骤如下:
- 每个线程读取主内存中的共享数据作为副本存入本地私有内存中;
- 每个线程分别操作自己本地私有内存中的副本数据;
- 每个线程将数据刷新到主内存中;
- 主内存中的共享数据发生变化;
根据步骤发现,图下的两个线程操作共享数据时,共享数据本应该分别加1一次,最终应该为2;实际上数据只变成了1,这就是产生了非线程安全问题。
注意:本地内存存放的是主内存共享数据的副本。
JMM(内存模型)的多线程可见性分析
JMM(内存模型)的可见性,步骤分析:
- 线程T1、T2将主内存中的共享数据读取到自己的本地私有内存中存放;
- 线程T1操作地私有内存中副本数据;
- 线程T1将数据刷新到主内存中;
- 主内存中的共享数据发生变化;
- 主线程通知线程T2;
- 线程T2中数据副本数据与主线程数据一致;
- 线程T2操作地私有内存中副本数据;
- 线程T2将数据刷新到主内存中;
- 主内存中的共享数据发生变化;
这个过程中其实相当与T2线程可以看到T1线程中变化后的数据,确保在T2线程操作时,数据时最新状态下的数据。
JMM(内存模型)三大特性与Volatile、Synchronized、Lock相关
原子性
原子性:保证基本数据类型的变量的读取、赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
原子性的延伸:通过 synchronized 和 Lock 可以实现更大范围操作的原子性。由于 synchronized 和 Lock 能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
可见性
普通共享变量:不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的;当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
Volatile修饰共享变量:具有可见性,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
synchronized、Lock 加锁:具有可见性,synchronized、Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
下面就来具体介绍下happens-before原则(先行发生原则):
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
volatile保证有序性:volatile关键字来保证一定的“有序性”。
synchronized、Lock 来保证有序性:synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。