目录
1、CPU和物理主内存的速度不一致
2、JMM规范下的三大特性
3 、多线程先行发生原则[happens-before]
4、volatile与JMM
由于CPU的运行速度远远领先于内存,所以CPU的运行并不是直接操作内存,而是先把内存里边的数据读到高速缓存,再进行操作;多线程场景下,一来二去,对内存的读和写操作的时候就会造成数据的不一致性问题。
JVM规范中试图定义一种Java内存模型(Java Memory Model,简称JVM)来屏蔽掉各种硬件和操作系统的对内存访问差异,以实现让Java程序在各种平台下都能到达一致的内存访问效果。
JMM本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量何时写入以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。
可见性:是指当一个线程修改了某个共享变量,其它线程是否能够立即知道该变更,JMM规定了所有的变量都存储在主内存中。
系统主内存共享变量数据修改被写入的时机是不确定的,在多线程并发下很可能出现“脏读”,所以每个线程都用自己的工作内存(高速缓存),线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在线程自己的工作过内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法访问对方工作过内存中的变量,线程间变量值的传递均需要通过主内存来完成。
原子性:指一个操作是不可打断的,及多线程环境下,操作不能被其他线程干扰——加锁。
有序性:指令重排序——其实就是为提高CPU的性能,编译器和处理器通常会对执行序列进行重新排序。Java规范规定JVM线程内部维持循序化语义,即只要程序的最终结果与它循序化执行的结果相等,那么指令的执行循序可以与代码循序不一致,此过程叫指令的重排序。
下图就是编译器和处理器对程序进行的重排序的可能
小结:
我们定义的所有共享变量都存储在物理内存中;
每个线程都有自己独立的工作过内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝);
线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级);
不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行。
如果一个操作happens-before另一个操作,那么第一个操作的执行结果对第二个可见,而且第一个操作的执行循序排在第二个操作之前。两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的循序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
happens-before的8条原则:
1.次序规则:一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作;
2.锁定规则:一个unLock操作先行发生于后面(这里的“后面”是指时间上的先后)对同一个锁的lock操作;
3.volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后。
4.传递规则:如果操作A先行发生于操作B,而操作B有先行发生于操作C,则可以得出操作A先行发生发生于操作C;
5.线程启动规则(Thread Start Rule)Thread对象的start()方法先行发生于此线程的每一个动作;
6.线程中断规则(Thread Interruption Rule)对线程interrupt()方法的调用优先发生于被中断线程的代码检测到中断事件的发生;可以通过Thread.interrupted()检测到是否发生中断,也就是说你要先调用interrupt()方法设置过中断标志位,我才能检测到中断发生。
7.线程终止规则(Thread Termination Rule)线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过isAlive()等手段检测线程是否已经终止执行。
8.对象终结规则(Finalizer Rule)一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
在Java语言里面,happens-before的语义本质上是一种可见性
A Happens-Before B意味着A发生过的事情对B来说是可见的,无论A事件和B事件是否发生在同一个线程里。
JMM的设计分为两个部分:
一部分是面向我们程序员提供的,也就是happens-before规则,他通俗易懂的向我们程序员阐述了一个强大内存模型,我们只要理解happens-before规则,就可以编写并发安全的程序了。
另一部分是针对JVM实现的,为了尽可能少的对编译器和处理器做约束从而提高性能,JMM在不影响程序执行结果的前提下对其不做要求,即允许优化重排序。我们只需要关注前者就好了,也就是理解happens-before规则即可,其他繁杂的内容有JMM规范结合操作系统给我们搞定,我们只写代码即可。
我们都知道被volatile修饰的变量都会对所有线程可见,并且禁止指令重排序
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量;
所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从内存中读取。实现的原理就是在读写时插入内存屏障
屏障类型 | 指令实例 | 说明 |
LoadLoad | Load1;LoadLoad;Load2 | 保证load1的读取操作在load2及后续读取操作之前执行 |
StoreStore | Store1;StoreStore;Store2 | 在store2及其后的写操作执行前,保证store1的写操作已经刷新到主内存 |
LoadStore | Load1;LoadStore;Store2 | 在store2及其后的写操作执行前,保证load1的读操作已读取结束 |
StoreLoad | Store1;StoreLoad;Load2 | 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行 |
Java内存模型中定义的8种每个线程自己的工作内存与主物理内存之间的原子操作
read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)
下图是8种原子操作的流程图
read:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
load:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
assign:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个变量赋值字节码指令时会执行该操作
store:作用于工作内存,将复制完毕的工作变量的值写回给主内存
write:作用于主内存,将store传输过来的变量值赋值给主内存的变量
由于上述6条只能保证单条指令的原子性;针对多条指令的组合性原子保证,没有大面基加锁,所以,JVM提供了另外两个原子指令:
lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程
unlock:作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
volatile为什么不具备原子性?
对于volatile变量具备可见性,JVM只是保证从主内存加载到线程工作内存的值是最新的,也仅是数据加载时是最新的。但是多线程环境下,“数据计算”和“数据赋值”操作可能多次出现,如数据在加载之后,若主内存volatile修饰变量发生修改之后,线程工作内存中的操作将会作废去读主内存最新值,操作出现写丢失问题。即各线程私有内存和主内存公共内存中变量不同步,进而导致数据不一致。由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须使用加锁同步。
加深理解!简单点来说就是,C/C++会在使用volatile修饰的变量前后进行指令[例如变量的写:StoreStore;变量操作;StoreLoad][变量的读:LoadLoad;变量操作;LoadStore]插入。
在每一个volatile写操作前面插入一个StoreStore屏障:StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存;当然volatile写与volatile写也禁止重排
在每一个volatile写操作后面插入一个StoreLoad屏障:StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写或普通写操作重排序;
在每一个volatile读操作后面插入一个LoadLoad屏障:LoadLoad屏障用来禁止处理器把"上面的volatile读"与下面的普通读重排序;"上面的volatile读"与下面的volatile读重排序
在每一个volatile读操作后面插入一个LoadStore屏障:LoadStore屏障用来禁止处理器把"上面的volatile读"与下面的普通写重排序。"上面的volatile读"与下面的volatile写重排序
volatile写之前的操作,都禁止重排序到volatile之后;
volatile读之后的操作,都禁止重排序到volatile之前;
volatile写之后volatile读,禁止重排序
volatile适用场景:如修饰boolean,int类型的变量;DCL(双重检查锁)初始化单例模式时,需要加上volatile,就怕对象还没来得及赋值,上下文切换,造成拿取单例对象时空指针。
海棠春睡梅妆惹落花,悠悠一抹斜阳吹尺八。
今天先这个样子,之后再调整本文,希望大家多多指正~交换想法之后,我们的总和将大于2。