# JMM&volatile
# 1. 什么是JMM?
java内存模型(JMM)是一种抽象的概念,并不是真实存在的,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(栈空间),用于存储线程私有的数据,而java内存模型中规定所有所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问主内存,但线程对变量的操作必须在工作内存中进行,首先要将变量从主内存拷贝 到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存的变量副本拷贝。
工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程见的通信(传值)必须通过主内存来完成。
# 2. JMM不同于JVM内存区域模型
JMM与JVM内存区域的划分是不同的概念层次,更恰当的说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性,可见性展开。JMM与JVM内存区域唯一相似处,都存在共享数据区域和私有数据区域,在J MM中主内存数据共享数据区域,从某个程度上将应该包含了堆和方法区,而工作内存数据是线程私有数据区域,从某个程度上将,应该包括程序计数器、虚拟机栈以及本地方法栈。
线程、工作内存、主内存的工作交互图:
每个线程都有自己独有的工作内存,JMM允许线程访问主内存中的共享变量,并将其拷贝到自己独有的工作内存中,对变量进行操作后,再将共享变量副本写回到主内存。
# 3. 为什么需要有JMM?
线程是OS的最小调度单位,所有程序运行时的实体,本质上都是一条条线程,每个线程创建时,JVM都会为其创建一个工作内存(也可以理解为虚拟机栈),用于存储线程私有的数据,线程如果想要操作主内存中的某个变量,此时必须借助于JMM,通过工作内存间接完成。
# 4. 内存交互操作
4.1 JMM定义了八种操作来完成将一个变量从主内存拷贝到工作内存、从工作内存回写到主内存的过程。
lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
unlock(释放):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以遍随后的load动作使用。
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以遍随后的write的操作。
write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
4.2 java内存模型还规定再执行上述八种基本操作时,必须满足如下规则:
如果要把一个变量从主内存中赋值到工作内存,就需要按顺序的执行read和load操作,如果把变量从工作内存这种同步写回主内存中,就要按顺序的执行store和write操作。但java内存模型只要求上述操作必须按顺序执行,而没有保证必须时连续执行。
不允许read和load,store和write操作之一单独出现。
不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load和assign)的变量。即对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须是成对出现。
如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值。
如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
4.3 数据从主内存到工作内存,再回到主内存的示意图:
# 5. JMM中的happens-before原则
程序顺序规则:一个程序内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作之前,也就是说,如果对一个锁解锁后,再枷锁,那么枷锁的动作必须再解锁动作之后(同一个锁);
volatile规则:对一个变量的写操作先行发生于后面对这个变量的读操作。简单的理解:volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新的值。
传递规则:A先于B,B先于C,那么A必然先于C。
线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。即如果线程A,在执行线程B的start方法前修改了共享变量的值,那么当线程B执行start方法时,线程A变更过的共享变量,对线程B可见。
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
线程终止规则:线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改对线程A可见。
对象终结规则:对象的构造函数执行,结束先于finalize()方法。
# 6. JMM的三大特性
# 6.1 原子性
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
注意事项:32位系统中,byte、short、int、float、boolean、char等基本数据类型的读写是院子操作,但long、double类型的数据,它们的读写并非原子性的。因为long、double是64位的存储单元。
计算机在执行程序时,为了提高性能,编译器和处理器会对指令进行重排序,一般分为三种:
编译器优化的重排:编译器在不改变单线程程序语义的前提下,可以重新安排语句的 执行 顺序。
指令并行的重排:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖行,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排:由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存和缓存的数据同步存在事件差。
# 6.2 可见性
当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。
无论是编译器优化还是处理器优化的重排线程,在多线程环境下,都会导致程序乱序执行的情况出现。
# 6.3 有序性
对于单线程执行的代码,代码总是按顺序依次执行的。就算发生指令重排,也不会影响单线程的执行结果。
# 7. JMM是如何解决原子性、可见性、有序性问题的?
volatile:可以解决可见性和有序性,但不能解决原子性。
synchronized:可以解决原子性,但不能解决指令重排。
# 8. volatile关键字
# 8.1 volatile关键字保证可见性
volatile可以保证可见性和禁止指令重排,但它不能保证原子性。
当写一个volatile变量时,JMM会把该线程工作内存中的共享变量值刷新到主内存中;当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程只能从主内存中重新读取共享变量。
# 8.2 volatile是如何禁止指令重排序?
volatile是通过内存屏障实现其在内存中的语义,即可见性和禁止指令重排。
内存屏障:又称内存栅栏,是一个CPU指令,它的作用有三个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性,三是强制刷出各种cpu的缓存数据,因此任何cpu上的线程都能读取这些数据的最新版本。
内存屏障规则:
LoadLoad屏障:(指令Load1;LoadLoad;Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
LoadStore屏障:(指令Load1;LoadStore;Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:(指令Store1;StoreStore;Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
StoreLoad屏障:(指令Store1;StoreLoad;Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障种最大的。在大多数处理器的实现种,这个屏障是个万能屏障,兼具其他三种内存屏障的功能。
注意:volitale并没有直接使用OS的内存屏障指令,而是使用JVM内存屏障字节码指令,JVM的内存屏障字节码指令会间接使用OS的内存屏障指令。
经典例子:双重锁单例
// 未加volalite
public class Singleton{
private static Singleton singleton;
private Singleton(){}
public static Singleton getSingleton(){
if(singleton==null) {
synchronized(Singleton.class){
if(singleton==null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
在高并发的情况下时存在线程安全的问题:
singleton = new Singleton();
可以分为三步执行:
memory = allocate();//1. 分配对象内存空间
instance(memory);//2. 初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null
由于步骤2和步骤23可能会重排序,如下
memory = allocate();//1. 分配对象内存空间
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null,但对象还没有初始化
instance(memory);//2. 初始化对象
这是因为步骤2和步骤3不存在依赖关系,而且无论重排前还是重排后程序的执行结果在单线程种并没有改变,因此这种重排优化是允许的。
解决方案:使用volatile关键字,禁止指令重排。
public class Singleton{
private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getSingleton(){
if(singleton==null) {
synchronized(Singleton.class){
if(singleton==null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
← 输出流 Synchronized →