1.Volatile是什么?java
2.Volatile有哪些特性?编程
3.Volatile每一个特性的底层实现原理是什么?数组
缓存一致性协议:MESI缓存
因为计算机储存设备(硬盘等)的读写速度和CPU的计算速度有着几个数量级别的差距,为了避免让CPU停下来等待读写,在CPU和存储设备之间加了高速缓存,每一个CPU都有本身的高速缓存,并且他们共享同一个主内存区域,当他们都要同步到主内存时,若是每一个CPU缓存里的数据都不同,这时应该以哪一个数据为准呢?为了解决这一同步问题,须要各个处理器都遵循必定的协议,好比MSI,MOSI,MESI等,目前用的比较多的就是MESI协议。多线程
注 :缓存一致性协议是在总线上实现的。并发
MESI是表明了缓存数据的四种状态,分别是Modified、Exclusive、Shared、Invalid:
①M(Modified):被修改的,处于这一状态的数据,只在本CPU中有缓存数据,而其余CPU中没 有。同时其状态相对于内存中的值来讲,是已经被修改的,且没有更新到内存中。ide
②E(Exclusive):独占的,处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改, 即与内存中一致。性能
③S(Shared):共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致。学习
④I(Invalid):要么已经不在缓存中,要么它的内容已通过时。为了达到缓存的目的,这种状态 的段将会被忽略。一旦缓存段被标记为失效,那效果就等同于它历来没被加载到缓存中。在 缓存行中有这四种状态的基础上,优化
<font color='red'>总结:</font>每一个处理器经过嗅探在总线上传递的数据来检查本身缓存的数据是否过时,当处理器发现本身缓存行数据对应的内存地址被修改,就会将当前缓存行里的数据设置为无效。当再次须要使用该数据的时候就会去主内存中从新读取数据。
Java内存模型:JMM
Java内存模型规定了Java变量(实例字段,静态字段,构成数组对象的元素等,但不包括局部变量和方法参数)存储到内存和从内存中取出的的底层实现细节,这些变量都存储在主(Main Memory)中,(主内存只是虚拟机内存的一部分)
每一个线程都有本身的工做内存(Working Memory),工做内存(实际上工做内存并不存在,他只是JMM抽象出来的一个概念)中保存着从主内存读取来的变量副本拷贝
,线程对副本的操做(读取,修改,赋值)都要在工做内存中进行,而不能直接在主内存中进行,不一样线程之间也不能访问彼此的工做内存。且线程之间变量值的传递须要通过主内存做为第三方中介。
内存间原子性交互操做:
①lock(锁定):做用于主内存上的变量,当一个变量被标识为Lock的时候,表示该变量是线程 独占状态,此时其余线程不能够对该变量进行操做。早前的缓存一致性协议就是这样,可是这 样会致使某个变量被一个线程占用,其余线程不能够对其进行访问,并发就变成了串行,效率 下降,在后来的缓存一致性协议中就抛弃了这种作法。
②unlock(解锁):一样是做用于主内存中的变量,使变量从锁定状态释放出来,其余线程才可 以对其操做。
③read(读取):读取主内存中的变量,传输到线程的工做内存,等待后续的load操做。
④load(加载):加载工做内存中的变量,把其放入工做内存的副本变量中。
⑤use(使用):把工做内存中的变量副本值传递给执行引擎
,每当虚拟机遇到使用变量值字节码 的时候就会进行此操做。
⑥assign(赋值):把一个从执行引擎接收到的数据赋给工做内存的变量,即执行赋值操做。
⑦store(存储):把工做内存中通过赋值更新后的值传递到主内存中,为后续write作准备。
⑧write(写入):把store操做传递来的值写入主内存,替换以前的值,完成同步更新。
4.JMM并发的特性要求:
①可见性(Visibility):可见性要求是指当一个线程修改了共享变量的值之后,其余线程可以马 上得知这个修改。
②原子性(Atomicity):原子性是指对变量的操做(read,load,assign等上述交互操做)不 可分割不可被打断,每一个操做都要完整的执行完成才能够有其余操做进来。且默认对基本数据类 型的访问和读写都是原子性的(64位的long型和double型会有可能被拆分红两个32位进行读写 操做,可是这种几率极低,能够忽略不计。)
③有序性:为了提高效率,编译器会对代码进行乱序优化,而CPU会乱序执行,可是这样的操做 会致使很严重的问题。为了解决这一问题,使用了内存屏障来防止乱序的发生。 这样按照顺序执 行就是有序性。
进入主题
Volatile是轻量级的synchronized锁,所谓轻量级,是由于synchronized使用时会引发线程的上下文切换,使得执行成本更高,效率更低,而Volatile不会有这些问题,效率更高。
1.可见性 :Visibility
①定义:当一个线程修改了共享变量的值之后,其余线程可以立刻得知这个修改。
②先看一个例子:
package Test; public class VolatileTest { public static boolean flag = false; public static void main(String[] args) throws InterruptedException { new Thread(new Runnable() { @Override public void run() { System.out.println("主线程等待线程A修改数据~~~"); while (!flag){} System.out.println("主线程发现数据被线程A修改~~~~"); } }).start(); Thread.sleep(300); new Thread(new Runnable() { @Override public void run() { changeData(); } }).start(); } public static void changeData(){ flag = true; System.out.println("线程A修改完成数据~~~~"); } } // 执行结果: 等待线程线程A修改数据~~~ 线程A修改完成数据~~~~
能够看出,当线程A修改完成数据后,另一个线程应该要输出主线程发现数据被线程A修改~~~~
,可是实际的运行状况是主线程一直处于等待状态。而若是把public static boolean flag = false;
修改成public static volatile boolean flag = false;
,也就是把变量用volatile
修饰,此时的执行结果:
等待线程线程A修改数据~~~ 线程A修改完成数据~~~~ 线程A修改数据完成~~~~
很明显,线程A修改变量后,主线程也能感知到,使得数据具备可见性,这就是volatile的做用。
③volatile可见性底层实现原理:
对未加volatile修饰的变量修改时的底层汇编码:
对volatile修饰的变量修改时的底层汇编码:
由底层汇编可知,对volatile修饰的变量修改时,汇编指令前面会多一个lock
前缀,这个lock 前缀将会致使下面两件事发生:
(1)当即将修改过的数据回写到主内存中,刷新原来的数据。
在Pentium及Pentium以前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其余处理器暂时没法经过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上作了一个颇有意义的优化:若是要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),而且该内存区域被彻底包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。因为在指令执行期间该缓存行会一直被锁定,其它处理器没法读/写该指令要访问的内存区域,所以能保证指令执行的原子性。这个操做过程叫作缓存锁定(cache locking),缓存锁定将大大下降lock前缀指令的执行开销,可是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。 ps:摘自https://blog.csdn.net/yu280265067/article/details/50986947
(2)若是其余处理器缓存了这个被修改过的数据,那回写操做会使他们失效。
IA-32处理器和Intel64位处理器使用MESI(缓存一致性)维护内部缓存和其余处理器缓存的一致性,在多核处理器(多线程)中,处理器和线程使用嗅探技术检测各自缓存中的数据和总线上传递的数据是否一致,若是检测到有其余处理器或线程回写数据,且该数据是共享数据,那么就会强制使其余缓存了该数据的缓存中的数据失效。
2.有序性(禁止指令重排序):Odering
(1)指令重排序:为了优化和性能,编译器和处理器常常会对指令作重排序,且分为三种。
①编译器重排序:在不改变单线程程序语义的前提下,从新安排代码执行顺序。
②指令级并行重排序:处理器采用指令级并行技术将多条指令重叠执行,若是数据不存在 依赖,能够改变机器指令执行。
③内存系统重排序:处理器使用缓存和读/写缓冲区,使得加载和存储看上去是乱序执行。
重排序顺序示意图
先看一个例子:
package Test; public class NoReoder { private static int a = 0; private static boolean flag = false; public static void main(String[] args) throws InterruptedException { new Thread(new Runnable() { @Override public void run() { write(); } }).start(); new Thread(new Runnable() { @Override public void run() { read(); } }).start(); } public static void write(){ a = 30; flag = true; System.out.println("方法一结束~~"); } public static void read(){ if (flag ) { a = a + 10; System.out.println(a); System.out.println("方法二结束~~"); } } }
当线程A启动调用了write方法,线程B启动调用read方法时能不能知道a被write方法修改了呢?答案是:不必定!!!
因为write方法中:
a = 30; flag = true;
这两个操做的数据没有依赖性,因此可能会被重排序为:
flag = true; a = 30;
这样就会使得read方法先读到flag = true ,而 a 还没修改完,从而使计算结果出错。
为了解决这种问题,在JMM中设计了内存屏障技术:
简单来讲,Volatile的有序性就是靠内存屏障来实现,就是把一些操做限制在某些操做以前或者以后,好比将Store操做限制在Load以前,这样就能让其余线程获得的数据是最新的或者须要先写入数据再让其余线程加载数据。
相关参考,详见《Java并发编程的艺术》一书
本文仅是对我的学习中一些理解的记录,鉴于水平有限或多或少存在错漏或不严谨之处,欢迎各位大神批评指正。码字不易,欢迎转载转发但请标注出处。
但愿病毒早点结束,再难的日子里也要坚持学习,新年快乐,最后愿工做在与病毒抗争最前线的医护人员平安打完这场仗,加油!!!!