相比synchronized,final和volatile也是常用的关键字,下面聊一聊这两个关键字的使用和实现html
1.使用java
final使用:缓存
例子:闭包
class FinalFieldExample { final int x; int y; static FinalFieldExample f; public FinalFieldExample() { x = 3; y = 4; } static void writer() { f = new FinalFieldExample(); } static void reader() { if (f != null) { int i = f.x; int j = f.y; } } }
调用reader方法的线程保证了当f不为null时,x的值必定能够读取到,由于x声明为了final,而y则不必定并发
volatile使用:app
JSR133 FAQ中例子1:函数
class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { //uses x - guaranteed to see 42. } } }
上边这个例子中,一个线程调用writer方法,一个线程调用reader发放,当先调用writer方法,后调用reader方法时,因为对象v声明为volatile,具备可见性,也就是一个线程的修改会当即在另外一个线程中体现出来,所以reader方法中断定会为true,若是进入该分支后,保证x的值必定为42,由于volatile保证了禁止指令重排,因此writer中第一个赋值必定会在第二个赋值前执行。优化
JSR133 FAQ中例子2:this
private volatile static Something instance = null; public Something getInstance() { if (instance == null) { synchronized (this) { if (instance == null) instance = new Something(); } } return instance; }
以上是一个典型double-check locking例子,instance声明为volatile保证了构造Something对象的指令和赋值给instance的指令不会重排,这样的话当其余线程拿到instance的引用不为null时,instance已经初始化完毕了spa
2.规则和原理
在解释下面规则原理以前仍是要在说明一下,编译器和处理器为了优化程序执行的速度,会对指令进行重排序,下面经过一个例子来讲明:
public class PossibleReordering { static int x = 0, y = 0; static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { Thread one = new Thread(new Runnable() { public void run() { a = 1; //1 x = b; //2 } }); Thread other = new Thread(new Runnable() { public void run() { b = 1; //3 y = a; //4 } }); one.start();other.start(); one.join();other.join(); System.out.println(“(” + x + “,” + y + “)”); }
通常可能认为,这个代码的执行结果可能有三种,分别是(1,0),(0,1),(1,1)(虽然这种状况没有跑出来)这三种状况,可是当连续执行10000屡次的时候,发现竟然有(0,0)这种状况,实际上这是由于指令在执行的时候发生了重排序,也就是说编译器和处理器会根据实际状况优化代码执行的顺序。指令重排序是以as if serial优化的,因此只要保证在单线程下,最后的执行结果一致便可。上面这个例子就是发生了重排序,若是步骤1和步骤2发生重排序,致使实际执行顺序为2->3->4->1,那么就会出现(0,0)
JSR133(JMM)中对final域在重排序方面进行了约束,以保证final的正确使用
final规则
当final域为对象的时候,编译器和处理器须要遵循这两个重排序原则:
看下面的例子:
public class FinalExample { int i; //普通变量 final int j; //final变量 static FinalExample obj; public void FinalExample () { //构造函数 i = 1; //写普通域 j = 2; //写final域 } public static void writer () { //写线程A执行 obj = new FinalExample (); } public static void reader () { //读线程B执行 FinalExample object = obj; //读对象引用 int a = object.i; //读普通域 int b = object.j; //读final域 } }
第一条规则实际上表达的是对final域的写入不能够重排序到构造函数外,这一条本质上包含了下面两条规则:
所以当线程B执行的时候(不考虑读取时候的重排序),当读取object引用时,对象内到final域已经初始化好了,能够正常读取,可是普通域可能没有初始化好
第二条规则一样也须要在编译器和处理器层面去保证:
所以当线程B执行的时候,读取对象引用和读取对象中的普通域可能发生重排,而读取对象引用和对象中的final域不会,这样经过和第一条结合时候,对于final域,并发状况下,能够保证final域的正常读取
上面看到对final域对对象实际上是基础类型,若是是引用类型呢
public class FinalReferenceExample { final int[] intArray; //final是引用类型 static FinalReferenceExample obj; public FinalReferenceExample () { //构造函数 intArray = new int[1]; //1 intArray[0] = 1; //2 } public static void writerOne () { //写线程A执行 obj = new FinalReferenceExample (); //3 } public static void writerTwo () { //写线程B执行 obj.intArray[0] = 2; //4 } public static void reader () { //读线程C执行 if (obj != null) { //5 int temp1 = obj.intArray[0]; //6 } }
}
对于final域为引用对象的状况,编译器和处理器有下面对重排序限制:
咱们先执行线程A,再执行线程B、最后执行线程C,因为重排序的限制,步骤3与步骤1,步骤3与步骤2不可重排序,而步骤1和步骤2存在关联关系,所以线程C执行的时候能够正常读取到final域引用对象的成员值。而线程B的修改是否能够在线程C中读取到则不必定了,须要在线程B、C之间须要使用同步原语
逃逸
上面咱们经过例子说明了一个问题,构造函数中的final域引用不可逃脱出构造函数,那么若是经过其余方式将构造对象暴露出去呢,请看下面这个例子:
public class FinalReferenceEscapeExample { final int i; static FinalReferenceEscapeExample obj; public FinalReferenceEscapeExample () { i = 1; //1写final域 obj = this; //2 this引用在此“逸出” } public static void writer() { new FinalReferenceEscapeExample (); } public static void reader { if (obj != null) { //3 int temp = obj.i; //4 } } }
上面这个例子中,final域的重排序限制没法限制步骤1和步骤2的重排序,那么就有可能出现逃逸现象,当reader线程执行时,可能没法正常访问到构造对象中final域初始化后的值
volatile规则
为了达到java跨平台的语言特性,须要将内存从新抽象,这样就诞生了jsr133,jsr133描述了java内存模型,屏蔽了底层实现的差别,保证相同的代码在不一样平台上具备相同的表现。根据java内存模型(java memory model,简称JMM)的规定,能够简化为几个happen-before原则,happen-before先后两个操做不可重排序而且前者对后者内存可见:
happen-before原则是对java内存模型对近似描述,更严谨的java模型定义参考jsr133。jsr133对volatile语意进行了扩展,特别是关于重排序这方面,具体限制以下:
第二项操做指的是第一项操做后面的全部操做,例如,普通的读写操做不可与以后的volatile变量的写操做重排序,参考上面volatile例子,留白的单元格表示在保证java语意不变的状况下能够重排序,例如,java语意不容许对同一个对象的读写重排序,可是对不一样对对象的读写能够
内存屏障
内存屏障(memory barrier,也称做内存栏栅)是一种CPU指令,用于控制指令重排序和解决可见性问题
内存屏障能够被分为如下几种类型
上面的重排序规则能够经过内存屏障指令实现:
总的来讲,内存屏障指令提供了两个方面的功能:
第一条,咱们已经在上面阐明了,对于第二条功能是经过缓存一致性协议达到,缓存一致性协议在单机多核的状况下是经过硬件实现。最为出名的缓存一致性协议是Intel的MESI。
三、总结
final和volatile语意在jsr133中作了相应扩展,保证了其语意的正确性。正确理解其使用规则和编译器和处理器实现原理对咱们平常工做有意义,不论是final仍是volatile底层都依赖内存屏障技术,内存屏障技术(指令)最重要的功能就是对指令重排序对限制,对于volatile对语意中可见性语意,经过内存屏障技术和缓存一致性协议实现。
参考:
http://www.infoq.com/cn/articles/java-memory-model-6
http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html
https://tech.meituan.com/java-memory-reordering.html
http://www.cnblogs.com/dolphin0520/p/3920373.html