相比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