1. 并发编程的3个概念
并发编程时,要想并发程序正确地执行,必需要保证原子性、可见性和有序性。只要有一个没有被保证,就有可能会致使程序运行不正确。html
1.1. 原子性
原子性:即一个或多个操做要么所有执行而且执行过程当中不会被打断,要么都不执行。java
一个经典的例子就是银行转帐:从帐户A向帐户B转帐1000元,此时包含两个操做:帐户A减去1000元,帐户B加上1000元。这两个操做必须具有原子性才能保证转帐安全。假如帐户A减去1000元以后,操做被打断了,帐户B却没有收到转过来的1000元,此时就出问题了。 编程
1.2. 可见性
可见性:即多个线程访问同一个变量时,一个线程修改了这个变量的值,其余线程可以当即看获得修改的最新值。数组
例以下段代码,线程1修改i的值,线程2却没有当即看到线程1修改的i的最新值:缓存
//线程1执行的代码 int i = 0; i = 10; //线程2执行的代码 j = i;
假如执行线程1的是CPU1,执行线程2的是CPU2。当线程1执行 i=10
时,会将CPU1的高速缓存中i的值赋值为10,却没有当即写入主内存中。此时线程2执行 j=i
,会先从主内存中读取i的值并加载到CPU2的高速缓存中,此时主内存中的i=0,那么就会使得j最终赋值为0,而不是10。安全
1.3. 有序性
有序性:即程序执行的顺序按代码的前后顺序执行。多线程
例以下面这段代码:并发
int i = 0; boolean flag = false; i = 1; flag = true;
在代码顺序上 i=1
在 flag=true
前面,而 JVM 在真正执行代码的时候不必定能保证 i=1
在flag=true
前面执行,这里就发生了指令重排序
。app
指令重排序
通常是为了提高程序运行效率,编译器或处理器一般会作指令重排序:ide
- 编译器重排序:编译器在不改变单线程程序语义的前提下,能够从新安排语句的执行顺序
- 处理器重排序:若是不存在数据依赖性,处理器能够改变语句对应机器指令的执行顺序。CPU 在指令重排序时会考虑指令之间的数据依赖性,若是指令2必须依赖用到指令1的结果,那么CPU会保证指令1在指令2以前执行。
指令重排序不保证程序中各个语句的执行顺序和代码中的一致,但会保证程序最终执行结果和代码顺序执行的结果是一致的。好比上例中的代码, i=1
和 flag=true
两个语句前后执行对最终的程序结果没有影响,就有可能 CPU 先执行 flag=true
,后执行 i=1
。
2. java 内存模型
因为 volatile 关键字是与 java 内存模型相关的,所以了解 volatile 前,须要先了解下 java 内存模型相关概念
2.1. 硬件效率与缓存一致性
计算机执行程序时,每条指令都是在 CPU 中执行的,而执行指令过程当中,势必涉及到数据的读取和写入。CPU 在与内存交互时,须要读取运算数据、存储结果数据,这些 I/O 操做的速度与 CPU 的处理速度有几个数量级的差距,因此不得不加入一层读写速度尽量接近 CPU 运算速度的高速缓存(Cache)来做为内存与 CPU 之间的缓冲:将运算须要使用的数据复制到高速Cache中;运算结束后再从高速Cache同步回内存中。这样 CPU 就无需等待缓慢的内存读写了。
这在单线程中运行是没有问题的,但在多线程中运行就引入了 缓存一致性
的问题:在多处理系统中,每一个处理器都有本身的高速缓存,而它们又共享同一主内存。当多个 CPU 的运算任务都涉及同一主内存区域时,将可能致使各自的缓存数据不一致,此时同步回主内存时以谁的数据为准呢?
为了解决缓存一致性问题,一般有两种解决方法:
- 在总线加 LOCK# 锁的方式
- 缓存一致性协议
早期的 CPU 中,经过在总线上加 LOCK# 锁的形式来解决,由于 CPU 在和其余部件通讯时都是经过总线进行,若是对总线加 LOCK# 锁,也就阻塞了 CPU 对其余部件访问(如内存),而使得只能有一个 CPU 使用这个变量的内存。
但这种方式有一个问题,在锁住总线期间,其余 CPU 没法访问内存,致使效率低下。
全部就出现了缓存一致性协议,最著名的就是 Intel 的 MESI 协议,MESI协议保证了每一个缓存中使用的共享变量的副本是一致的, 它的核心思想是:CPU写数据时,若是操做的变量是共享变量(其余 CPU 的高速缓存中也存在该变量的副本),会发出信号通知其余 CPU 将该变量的缓存设置为无效状态,那么当其余 CPU 读取该变量时,就会从内存从新读取。
JVM 有本身的内存模型,在访问缓存时,遵循一些协议来解决缓存一致性的问题。
2.2. 主内存和工做内存
Java虚拟机规范中试图定义一种 Java 内存模型(JMM, Java Memory Model)来屏蔽硬件和操做系统的内存访问差别,实现 Java 程序在各类平台上达到一致的内存访问效果。
Java 内存模型主要目标:是定义程序中各个变量的访问规则,即存储变量到内存和从内存中取出变量这样的底层细节。为了较好的执行性能,Java 内存模型并无限制使用 CPU 的寄存器和高速缓存来提高指令执行速度,也没有限制编译器对指令作重排序。也就是说:在 Java 内存模型中,也会存在缓存一致性问题和指令重排序问题。
Java 内存模型规定全部的变量(包括实例字段、静态字段、构成数组对象的元素,不包括线程私有的局部变量和方法参数,由于这些不会出现竞争问题)都存储在主内存中,每条线程有本身的工做内存(可与以前将讲的CPU高速缓存类比),线程的工做内存中保存了被该线程使用到的变量的主内存拷贝副本。线程对变量的全部操做(read,write)都必须在工做内存中进行,而不能直接读写主内存中的变量,线程间变量值的传递须要经过主内存来完成。如图所示:
2.3. JMM如何处理原子性
像如下语句:
x = 10; //语句1 y = x; //语句2 x++; //语句3 x = x + 1; //语句4
只有语句1才是原子性的操做,其余都不是原子性操做。
语句1是直接将10赋值给x变量,也就是说线程执行这个语句时,会直接将10写入到工做内存中。
语句2包含了两个操做,先读取x的值,而后将x的值写入到工做内存赋值给y,这两个操做合起来就不是原子性操做了。
语句3和4都包括3个操做,先读取x的值,而后加1操做,最后写入新值。
单线程环境下,咱们能够认为整个步骤都是原子性的。但多线程环境下则不一样,只有基本数据类型的访问读写是具有原子性的,若是还须要提供更大范围的原子性保证,可使用同步代码块 -- synchronized 关键字。在 synchronized 块之间的操做具有原子性。
2.4. JMM如何处理可见性
Java 内存模型是经过变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存做为传递媒介的方式实现可见性的。普通变量和 volatile 变量都如此,区别在于:
- volatile 特殊规则保证了新值能当即同步回主内存,以及每次改前当即从主内存刷新。所以 volatile 变量保证了多线程操做时变量的可见性
- 而普通变量没法保证这一点,由于普通的共享变量修改后,何时同步写回主内存是不肯定的,其余线程读取时,此时内存中的可能仍是原来的旧值。
除了 volatile 变量外,synchronized 和 final 关键字也能实现可见性。
synchronized 同步块的可见性是由:对一个变量执行 unlock 操做前,必须先把此变量同步回主内存中
这条规则得到的。
final 可见性是指:被final修饰的字段在构造器中一旦初始化完成,而且构造器没有把this的引用传递出去,那在其余线程中就能看见final字段的值
。
2.5. JMM如何处理有序性
Java 程序中自然的有序性可归纳为一句话:若是在本线程内观察,全部的操做都是有序的;若是在一个线程中观察另外一个线程,全部的操做都是无序的。前半句指:线程内表现为串行语义
,后半句是指:指令重排序
现象和工做内存和主内存同步延迟
现象
Java 中提供了 volatile 和 synchronized 关键字来保证线程之间操做的有序性。volatile 自己就包含了禁止指令重排序的语义,而 synchronized 是由一个变量在同一时刻只容许一条线程对其 lock 操做
这条规则得到,这条规则决定了持有同一个锁的两个同步代码块只能串行的执行。
happens-before 原则
Java内存模型中,有序性保证不只只有 synchronized 和 volatile,不然一切操做都将变得繁琐。Java 中还有一个 happens-before 原则
,它是判断线程是否安全的主要依据。依靠这个规则,能够保证程序的有序性,若是两个操做的执行顺序没法从 happens-before 原则中推导出来,则他们就不能保证有序性,能够随意重排序。
happens-before(先行发生)
是 Java 内存模型中定义的两项操做之间的偏序关系,若是操做A先行发生于操做B,那么就是说发生操做B以前,操做A产生的影响能被操做B观察到。影响包括修改内存中共享变量的值、发送了消息、调用了方法等。
下面是 Java 内存模型下的自然的先行发生关系,这些关系无需任何同步就已经存在:
程序次序规则
:一个线程内,按照代码顺序,书写在前面的操做先行发生于书写在后面的操做。准确的来讲,应该是控制流顺序,而不是代码顺序,由于要考虑分支、循环等结构管程锁定规则
:一个 unlock 操做先行发生于后面对同一个锁的 lock 操做volatile变量规则
:对一个 volatile 变量的写操做先行发生于后面对这个变量的读操做线程启动规则
:Thread 对象的 start() 方法先行发生于此线程的每一个一个动做线程终止规则
:线程中全部的操做都先行发生于线程的终止检测,咱们能够经过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程是否已经终止执行线程中断规则
:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可经过 Thread.isinterrupted() 检测是否有中断发生对象终结规则
:一个对象的初始化完成先行发生于他的 finalize() 方法的开始传递规则
:若是操做A先行发生于操做B,而操做B又先行发生于操做C,则能够得出操做A先行发生于操做C
第一条程序次序原则
,"书写在前面的操做先行发生于书写在后面的操做",这个应该是一段程序代码的执行在单线程中看起来是有序的,由于虚拟机可能对程序代码中不存在数据依赖性的指令进行重排序,但最终执行结果与顺序执行的结果是一致的。而在多线程中,没法保证程序执行的有序性。
第二条,第三条分别是关于 synchronized同步块 和 volatile 的规则。第四至第七条是关于 Thread 线程的规则。第八条是体现了 happens-before 原则的传递性。
下面是一个利用 happens-before 规则判断操做间是否具有顺序性的例子:
private int value=0; public void setValue() { this.value = value; } public int setValue() { return value; }
这段是一段普通的 getter/setter 方法,假如线程A先调用(时间上的前后)了 setValue(1),而后线程B调用了同一个对象的 getValue(),那么线程B的返回值是什么呢?
咱们按以上 happens-before 规则分析:
- 因为存在线程A和线程B调用,不在一个线程中,
程序次序原则
则不适用; - 没有同步快,也没有unlock和lock操做,因此
管程锁定规则
不适用; - 因为 value 没有被 volatile 修饰,因此
volatile变量规则
不适用; - 后面的线程启动、终止、中断、终结和这里没有关系;
- 因为没有适用的 happens-before 规则,最后的传递性也不适用
所以,能够断定尽管线程A在操做时间上先与线程B,但没法肯定线程B中 getValue() 的返回值,也就是说,这里的操做不是线程安全的。
该如何修复这个问题呢?能够有两种方法:
- 将 getter/setter 定义为 synchronized 方法,这样能够套用
管程锁定规则
- 使用 volatile 关键字修饰 value,这样能够套用
volatile变量规则
时间前后顺序和 happens-before 原则之间没有太大的关系,因此当咱们衡量并发安全问题时,不要受到时间顺序的干扰,一切应以 happens-before 原则为准。
3. volatile 实现原理
volatile 关键字是 JVM 提供的最轻量级的同步机制,当一个变量定义为 volatile 后,它将具备普通变量没有的两种特性:
保证此变量对全部线程的可见性
:当一个线程修改了该变量的值,新值对于其余线程来讲是能够当即得知的。禁止指令重排序优化
。普通变量只能保证在方法执行过程当中全部依赖赋值结果的地方都能得到正确的结果,而不能保证变量赋值操做的顺序和代码中的顺序一致,这也就是上文中提到的 Java 内存模型中所谓的"线程内表现为串行语义"。
3.1. volatile 保证原子性吗
基于 volatile 变量的运算在并发下并不必定是线程安全的。由于 Java 里的运算并不是原子操做,例以下面是一个 volatile 变量自增运算的例子:
public class VolatileTest { public static volatile int race = 0; public void increase() { race++; } public static void main(String[] args) { Thread[] threads = new Thread[20]; for(int i=0; i<20; i++){ threads[i] = new Thread( new Runnable() { @Override public void run() { for(int i=0; i<10000; i++) increase(); }; }); threads[i].start(); } while(Thread.activeCount()>1) // 等待全部累加的线程都结束 Thread.yield(); System.out.println(race); } }
这段代码发起了20个线程,每一个线程对 race 累加10000次,若是并发正确的话,输出结果应该是200000。而运行完这段代码后,每次输出的结果不同,都是小于200000。
问题就在于 race 经过 volatile 修饰只能保证每次读取的都是最新的值,但不保证 race++ 是原子性的操做,它包括读取变量的初始化,加1操做,将新值同步写到主内存 三步。自增操做的三个子操做可能会分开执行。
假如某时刻 race 值为10,线程A 对 race 作自增操做,先读取 race 的最新值10,此时 volatile 保证了 race 的值在此刻是正确的,但执行加1的时候,其余线程可能已经将 race 的值加大了,此时线程A工做内存中的 race 值就变成了过时的数据,而后将过时较小的新值同步回主内存。此时,多个线程对 race 分别作了一次自增操做,但可能主内存中的 race 值只增长了1。
volatile 没法保证对变量的任何操做都是原子性的,可使用 synchronized 或 java.util.concurrent 中的原子类来修改。
3.2. volatile 保证可见性吗
下面代码,线程A先执行,线程B后执行
//线程A boolean stop = false; while(!stop){ doSomething(); } //线程B stop = true;
这段代码在大多数时候,能将线程A中的while循环结束,但有时候也会致使没法结束线程,形成一直while循环。缘由在于:前面提到每一个线程都有本身的工做内存,线程A运行时,会将 stop=false 的值同步一份在本身的工做内存中。当线程B更新了stop的值为true后,可能还没来得及同步到主内存中,就去作其余事情了。此时线程B中 stop=true 的修改对于线程A是可不见的,致使线程A会一直循环下去。
若是将stop使用 volatile 修饰后,就能够保证线程A能退出循环。在于:使用 volatile 关键字会强制将线程B修改的新值stop当即同步至主内存。当线程B修改时,会致使线程A工做内存中stop的缓存行无效,反映到硬件上,就是CPU的高速缓存中对应的缓存行无效。线程A的工做内存中stop的缓存行无效后,会到主内存中再次读取变量stop的新值。从而 volatile 保证了共享变量的可见性。
3.3. volatile 保证有序性吗
volatile 能够经过禁止指令重排序来保证有序性,有两层意思:
- 当程序执行到 volatile 变量的读操做或写操做时:在其前面的操做确定所有已经完成,且结果对后面的操做可见。在其后面的操做确定还没进行
- 指令重排序优化时,不能将 volatile 变量前面的语句放在其后面执行,也不能将 volatile 变量后面的语句放到其前面执行。
举个例子以下,flag是 volatile 变量,x/y都是非 volatile 变量:
x = 2; //语句1 y = 0; //语句2 flag = true; //语句3 x = 4; //语句4 y = -1; //语句5
在指令重排序时候,由于flag是 volatile 变量。因此执行到语句3时,语句1和语句2一定是执行完成了,且执行结果对语句三、语句4和语句5是可见的。不会将语句3放到语句一、语句2前面,也不会将语句3放到语句四、语句5后面。语句1和语句2的顺序,语句4和语句5的顺序是不作保证的。
下面是一个指令重排序会干扰程序并发执行的例子:
Map config; volatile boolean init = false; // 变量定义为volatile // 线程A执行 // 读取配置信息,读取完后将init设置为true,以通知其余线程配置使用 config = loadConfig(); init = true; // 线程B执行 // 等待init为true,表明线程A已经将配置初始化好 while(!init) { sleep(); } doSomeThingWhihConfig(config); // 使用线程A中初始化好的配置信息
假如 init 变量没有使用 volatile 修饰,可能因为指令重排序的优化,致使线程A最后一句 init=true 提早执行(指这句代码对应的汇编代码被提早执行),这样线程B中使用配置信息的代码就可能出错。而使用 volatile 对 init 变量进行修饰,就能够避免这种状况,由于执行到 init=true 时,能够保证 config 已经初始化好了。
3.4. 内存屏障
volatile 关键字是如何禁止指令重排序的?关键在于有 volatile 关键字和没有 volatile 关键字所生成的汇编代码,加入 volatile 修饰的变量,赋值后会多执行一个lock前缀指令,这个指令至关于一个内存屏障
。经过内存屏障实现对内存操做的顺序限制,它提供了3个功能:
- 确保指令重排序时不会把后面的指令排到内存屏障以前的位置,也不会把前面的指令排序到内存屏障的后面。这样造成了指令重排序没法越过内存屏障的效果
- 强制将对工做内存的修改当即写入主内存
- 若是是写操做,会致使其余 CPU 中对应的缓存行无效
只有一个 CPU 访问内存时,不须要内存屏障;但若是有两个或更多 CPU 访问同一块内存,且其中一个在观察另外一个,就须要内存屏障来保证一致性了。
3.5. volatile 使用场景
某些状况下,volatile 同步机制的性能确实要优于锁(使用 synchronized 或 java.util.concurrent 包里面的锁),但因为对锁实现的不少优化和消除,使得很难量化的认为 volatile 会比 synchronized 快多少。若是 volatile 和本身比较的话,volatile 读操做的性能消耗与普通变量基本没有什么差异,但写操做可能慢一些,由于它须要在本地代码中插入许多内存屏障指令保证处理器不会乱序执行。即使如此,大多数场景下 volatile 的总开销仍然比锁低,volatile 没法保证操做的原子性,是没法替代 synchronized的。在 volatile 和锁之间选择的惟一依据是 volatile 的语义可否知足场景的需求。一般,使用 volatile 必须具有如下两个条件:
- 对变量的写操做不依赖于当前值,例如 count++ 这样自增自减操做就不知足这个条件
- 该变量没有包含在具备其余变量的不变式中