最先读《深刻理解java虚拟机》对于volatile部分就没有读明白,最近从新拿来研究并记录一些理解java
理解volatile前须要把如下这些概念或内容理解:git
一、JMM内存模型编程
二、并发编程的三问题:原子性、一致性、有序性缓存
三、先行发生原则安全
而后咱们结合上面的几个知识点来看volatile如何使用多线程
先看一下上面这张图片,即Java内存模型规定全部的变量都是存在主存当中(相似于前面说的物理内存),每一个线程都有本身的工做内存(相似于前面的高速缓存)。线程对变量的全部操做都必须在工做内存中进行,而不能直接对主存进行操做。而且每一个线程不能访问其余线程的工做内存并发
那么JMM为什么要如此设计?其主要缘由有两点:一、达到各平台访问内存效果的一致性 二、提高数据访问速度app
对于提高数据访问速度,主要用到了CPU高速缓存这部份内容:计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程当中,势必涉及到数据的读取和写入。因为程序运行过程当中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,因为CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,所以若是任什么时候候对数据的操做都要经过和内存的交互来进行,会大大下降指令执行的速度。所以在CPU里面就有了高速缓存jvm
在本文中,JMM可以帮助咱们理解为何会发生可见性问题函数
原子性指:一个操做执行时不能被打断或插入
好比i++,JVM指令包括3个操做:读取x的值,进行加1操做,写入新的值,若是并发执行i++,可能这三步操做不一样线程会穿插执行,原子性就是指,任何一个线程运行这三个操做时,其余线程不能进入运行这三步操做
如何解决原子性问题:
一、synchronized 二、Lock、其余锁
每一个线程都有各自的工做内存(高速缓存、详见JMM),线程A更改了变量的值后,线程B从本身的工做内存中获取变量的值还多是A修改前的值
如何解决可见性问题:
一、volatile关键字 二、Lock、synchronized
先看下什么是指令重排:处理器为了提升程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行前后顺序同代码中的顺序一致,可是它会保证程序最终执行结果和代码顺序执行的结果是一致的,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。若是程序不知足先行发生原则,那么可能发生指令重排
指令重排就影响了程序的有序性
如何解决有序性问题
一、volatile关键字 二、Lock、synchronized
从上面的三个问题来看volatile只能解决:可见性问题、有序性问题,但没法解决原子性问题,原子性问题仍须要锁的手段才能解决
先行发生原则(Happens-Before)是判断数据是否存在竞争、线程是否安全的主要依据,先行发生原则,能够帮你断定是否并发安全的,从而没必要去猜想是不是线程安全了
下面是Java内存模型中一些“自然的”先行发生关系,这些先行发生关系无需任何同步协助器协做java自带这些规则,能够直接在编码中使用。若是两个关系不在此列,而又没法经过这些关系推导出来,它们的顺序就没法保证,虚拟机能够对它们任意重排序
程序次序规则: 同一个线程内,按照代码出现的顺序,前面的代码 happens-before 后面的代码,准确的说是控制流顺序,由于要考虑到分支和循环结构。
管程锁定规则: 对于一个监视器锁的unLock操做 happens-before 于每一个后续对同一监视器锁的Lock操做。
volatile变量规则: 对volatile域的写入操做 happens-before 于每一个后续对同一个域的读操做。
线程启动规则: 在同一个线程里,对Thread.start的调用会 happens-before 于每个启动线程中的动做。
线程终结规则: 线程中的全部动做都 happens-before 于其它线程检测到这个线程已经终结,或者从Thread.join()调用成功返回,或者Thread.isAlive返回false.
中断规则: 一个线程调用另外一个线程的interrupt happens-before 于被中断的线程发现中断(经过抛出InterruptedException 或者调用isInterrupted和interrupted)
终结规则: 一个对象的构造函数的结束 happens-before 于这个对象finalizer的开始
传递性: 若是 A happens-before 于 B,且 B happens-before 于 C,则 A happens-before 于C。
其中比较重要且难以理解的几条是:
程序次序规则
一段程序代码的执行在单个线程中看起来是有序的。虽然这条规则中提到“书写在前面的操做先行发生于书写在后面的操做”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,由于虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,可是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。所以,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但没法保证程序在多线程中执行的正确性。
管程锁定规则
一个unlock操做先行发生于后面(时间上)对同一个锁的lock操做,也就是说不管在单线程中仍是多线程中,同一个锁若是出于被锁定的状态,那么必须先对锁进行了释放操做,后面才能继续进行lock操做
volatile变量规则
对一个volatile变量的写操做先行发生于后面(时间上)对这个变量的读操做,若是线程1写入了volatile变量v,接着线程2读取了v,那么,线程1写入v及以前的写操做都对线程2可见(线程1和线程2能够是同一个线程),能够当作是volatile解决可见性问题的描述
总结下来就是先行发生原则能够肯定两件事:
一、能帮助咱们判断程序是否线程安全
二、能帮助咱们肯定程序是否可能发生指令重排
有了以上知识储备咱们来看一下volatile如何正确的使用
一、多读单写
只有一个线程控制改变volitile变量的值,一个或多个线程并发读取volitile变量的值均可以用volitile
一般:线程开关或者状态标记的场景可使用
由于可见性保证了volatile多读单写的能力,但又由于volatile没有解决原子性问题的能力,因此不是多读多写
public static volatile boolean flag = false; //这种状况不添加volatile就有可能形成没法退出程序了 //添加了volatile就强制从主内存中得到值,就不会出现上述问题了 //这个例子体现:只有一个线程控制改变volatile变量的值 不少线程并发读取volitile变量的值均可以用volatile new Thread(() -> { while(!flag){ } System.out.println("退出了"); }).start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("setup"); flag = true;
//特别说明:我测试flag是非volatile,当不在while(!flag){上sleep,会一直循环,这种很是可能拿不到更改后的值,一直从工做内容中得到缓存值false。
二、防止指令重排
防止指令重排,一般:单例懒汉模式 double-check中使用
public class LazySingleton { private volatile static LazySingleton lazySingleton = null; private LazySingleton(){ } public static LazySingleton getInstance(){ if(lazySingleton == null){ synchronized (LazySingletonill.class){ if (lazySingleton == null) { lazySingleton = new LazySingleton(); } } } return lazySingleton; } public static void main(String[] args){ for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(LazySingleton.getInstance().hashCode()); }).start(); } } }
咱们来看一下为何不加volatile会引起指令重排的问题:
首先,这个出现问题的几率并不高,而且我经过jdk8的版本反编译并未和帖子内容一致,姑且先把帖子的原理写一下:
instance = new LazySingleton();,其实JVM内部已经转换为多条指令:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址可是通过指令重排序后以下:
memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址,此时对象还没被初始化
ctorInstance(memory); //2:初始化对象二、3步骤指令重排后发生了交换
假如线程A得到了锁而且正在执行lazySingleton = new LazySingleton();,这个实例化的jvm指令发生了重排,即instance = memory先于ctorInstance(memory)执行,恰好instance = memory执行完毕,线程B登场在执行if(lazySingleton == null){时为false,线程B return了一个没有初始化对象的实例出去,出现了返回不正确结果的现象