理解volatile其实仍是有点儿难度的,它与Java的内存模型有关,因此在理解volatile以前须要先了解有关Java内存模型的概念,目前只作初步的介绍。html
计算机在运行程序时,每条指令都是在CPU中执行的,在执行过程当中势必会涉及到数据的读写。数据库
咱们知道程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有CPU中执行指令的速度快,若是任何的交互都须要与主存打交道则会大大影响效率,因此就有了CPU高速缓存。缓存
CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。多线程
有了CPU高速缓存虽然解决了效率问题,可是它会带来一个新的问题:内存不可见性 ——> 数据一致性。app
线程在运行的过程当中会把主内存的数据拷贝一份到线程内部cache中,也就是working memory。这个时候多个线程访问同一个变量,其实就是访问本身的内部cache,再也不与主存打交道,只有当运行结束后才会将数据刷新到主存中。性能
【举一个简单的例子】:atom
public class VariableTest { public static boolean flag = false; public static void main(String[] args) throws InterruptedException { ThreadA threadA = new ThreadA(); ThreadB threadB = new ThreadB(); new Thread(threadA, "threadA").start(); Thread.sleep(1000l);//为了保证threadA比threadB先启动,sleep一下
new Thread(threadB, "threadB").start(); } static class ThreadA extends Thread { public void run() { while (true) { if (flag) { System.out.println(Thread.currentThread().getName() + " : flag is " + flag); break; } } } } static class ThreadB extends Thread { public void run() { flag = true; System.out.println(Thread.currentThread().getName() + " : flag is " + flag); } } }
运行结果:(光标一直在闪)spa
上面例子出现问题的缘由在于:操作系统
(1)线程A把变量flag(false)加载到本身的内部缓存cache中;线程B修改变量flag后,即便从新写入主内存,可是线程A不会从新从主内存加载变量flag,看到的仍是本身cache中的变量flag。因此线程A是读取不到线程B更新后的值,而后一直死循环...线程
(2)除了cache的缘由,重排序后的指令在多线程执行时也有可能致使内存不可见,因为指令顺序的调整,线程A读取某个变量的时候线程B可能尚未进行写入操做呢,虽然代码顺序上写操做是在前面的。
可是方案1存在一个问题:它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU可以运行,其余CPU都得阻塞,效率较为低下。
第二种方案:缓存一致性协议(MESI协议)确保每一个缓存中使用的共享变量的副本是一致的。
核心思想以下:当某个CPU在写数据时,若是发现操做的变量是共享变量,则会通知其余CPU告知该变量的缓存行是无效的,所以其余CPU在读取该变量时,发现其无效会从新从主存中加载数据。
共享变量是指:能够同时被多个线程访问的变量,共享变量是被存放在堆里面,全部的方法内的临时变量都不是共享变量。
二、有序性 (happens-before原则)
一个线程观察其余线程中的指令执行顺序,因为指令重排序,该观察结果通常杂乱无序
重排序:是指为了提升指令运行的性能,在编译时或者运行时对指令执行顺序进行调整的机制。重排序分为编译重排序和运行时重排序。
编译重排序是指:编译器在编译源代码的时候就对代码执行顺序进行分析,在遵循as-if-serial的原则前提下对源码的执行顺序进行调整。
运行时重排序:是指为了提升执行的运行速度,系统对机器的执行指令的执行顺序进行调整。
【as-if-serial原则】是指在单线程环境下,不管怎么重排序,代码的执行结果都是肯定的。
内存的可见性是指线程之间的可见性,一个线程的修改状态对另一个线程是可见的,用通俗的话说,就是假如一个线程A修改一个共享变量flag以后,则线程B去读取,必定能读取到最新修改的flag。
(看上边示例)
Java提供了volatile来保证可见性。
当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会当即被更新到主内存中,当其余线程读取共享变量时,它会直接从主内存中读取。
固然,synchronize和锁均可以保证可见性。
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操做。 即一个操做或者多个操做 要么所有执行而且执行的过程不会被任何因素打断,要么就都不执行。就像数据库里面的事务同样,他们是一个团队,同生共死。
在单线程环境下咱们能够认为整个步骤都是原子性操做,可是在多线程环境下则不一样,Java只保证了基本数据类型的变量和赋值操做才是原子性的。
要想在多线程环境下保证原子性,则能够经过锁、synchronized来确保。
// 一个很经典的例子就是银行帐户转帐问题:
好比从帐户A向帐户B转1000元,那么必然包括2个操做:从帐户A减去1000元,往帐户B加上1000元。试想一下,若是这2个操做不具有原子性,会形成什么样的后果。 假如从帐户A减去1000元以后,操做忽然停止(B此时并无收到)。 而后A又从B取出了500元,取出500元以后,再执行 往帐户B加上1000元 的操做。这样就会致使帐户A虽然减去了1000元,可是帐户B没有收到这个转过来的1000元(少了500)。 因此这2个操做必需要具有原子性(不可分割的最小操做单位)才能保证不出现一些意外的问题。
(1)volatile修饰的变量不容许线程内部cache缓存和重排序,能够保证内存的可见性和数据的一致性。
(2)线程读取数据的时候直接读写内存,同时volatile不会对变量加锁,所以性能会比synchronized好。
另外还有一个说法是使用volatile的变量依然会被读到线程内部cache中,只不过当B线程修改了flag后,会将flag写回主内存,同时会经过信号机制通知到A线程去同步内存中flag的值。
volatile相对于synchronized稍微轻量些,在某些场合它能够替代synchronized,可是又不能彻底取代synchronized,只有在某些场合才可以使用volatile。
使用它必须知足以下两个条件: // 一、对变量的写操做不依赖当前值; // 二、该变量没有包含在具备其余变量的不变式中
volatile常常用于两个两个场景:状态标记、double check(单例模式)