昨天看到一篇文章闡述技術類資料的"等級",看完以後頗有共鳴。再加上最近在工做中愈加以爲線程安全性的重要性和難以捉摸,又掏出了《Java併發編程實戰》研讀一番,這本書應該是屬於爲「JAVA 多線程做註解」的一本書,那我就爲書中關於對象安全發佈的內容做一些註解,做爲今年的第一篇博文。html
我讀的是中文版,確實感受書中有些地方的描述晦澀難懂,也沒有去拿英文原文來對照,就按中文版描述,再配上一些示例代碼記錄個人一些理解吧。java
發佈是個動詞,是去發佈對象。而對象,通俗的理解是:JAVA裏面經過 new 關鍵字 建立一個對象。編程
發佈一個對象的意思是:使對象在當前做用域以外的代碼中使用。好比下面knowSecrets指向的HashSet類型的對象,由static修飾,是一個類變量。當前做用域爲PublishExample類。緩存
import java.util.HashSet; import java.util.Set; /** * @author psj * @date 2019/03/10 */ public class PublishExample { public static Set<Secret> knowSecrets; public void initialize() { knowSecrets = new HashSet<>(); } }
public修飾引用knowSecrets,致使 在其餘類中也能訪問到這個HashSet對象,好比向HashSet添加元素或者刪除元素。所以,也就發佈了這個對象。安全
public class UsingSecret { public static void main(String[] args) { PublishExample.knowSecrets.add(new Secret()); PublishExample.knowSecrets.remove(new Secret()); } }
另外,值得注意的是:添加到HashSet集合中的Secret對象也被髮布了。多線程
由於對象通常是在構造函數裏面初始化的(不討論反射),當 new 一個對象時,會爲這個對象的屬性賦值,當前時刻對象各個屬性擁有的值 稱爲對象的狀態。併發
public class Secret { private String password; private int length; public Secret(){} public Secret(String password, int length) { this.password = password; this.length = length; } public static void main(String[] args) { //"current state" 5 組成了secObjCurrentState對象的當前狀態 Secret secObjCurrentState = new Secret("current state", 5); //改變 secObjCurrentState 對象的狀態 secObjCurrentState.setPassword("state changed"); } public void setPassword(String password) { this.password = password; } }
Secret對象有兩個屬性:password和length,secObjCurrentState.setPassword("state changed")
改變了對象的狀態。app
建立對象的目的是使用它,而要用它,就要把它發佈出去。同時,也引出了一個重要問題,咱們是在哪些地方用到這個對象呢?好比:只在一個線程裏面訪問這個對象,仍是有可能多個線程併發訪問該對象?ide
對象被髮布後,是沒法知道其餘線程對已發佈的對象執行何種操做的,這也是致使線程安全問題的緣由。函數
先看一個不安全發佈的示例----this引用逸出。參考《Java併發編程實戰》第3章程序清單3-7
當我第一次看到"this引用逸出"時,是懵逼的。後來在理解了「發生在先」原則、「初始化過程安全性」、"volatile關鍵字的做用"以後才慢慢理解了。這些東西后面再說。
外部類ThisEscape和它的內部類EventListener
public class ThisEscape { private int intState;//外部類的屬性,當構造一個外部類對象時,這些屬性值就是外部類狀態的一部分 private String stringState; public ThisEscape(EventSource source) { source.registerListener(new EventListener(){ @Override public void onEvent(Event e) { doSomething(e); } }); //執行到這裏時,new 的EventListener就已經把ThisEscape對象隱式發佈了,而ThisEscape對象還沒有初始化完成 intState=10;//ThisEscape對象繼續初始化.... stringState = "hello";//ThisEscape對象繼續初始化.... //執行到這裏時, ThisEscape對象纔算初始化完成... } /** * EventListener 是 ThisEscape的 非靜態 內部類 */ public abstract class EventListener { public abstract void onEvent(Event e); } private void doSomething(Event e) {} public int getIntState() { return intState; } public void setIntState(int intState) { this.intState = intState; } public String getStringState() { return stringState; } public void setStringState(String stringState) { this.stringState = stringState; }
如今要建立一個ThisEscape對象,因而執行ThisEscape的構造方法,構造方法裏面有 new EventListener對象,因而EventListener對象就隱式地持有外部類ThisEscape對象的引用。
那若是能在其餘地方訪問到EventListner對象,就意味着"隱式"地發佈了ThisEscape對象,而此時ThisEscape對象可能還還沒有初始化完成,所以ThisEscape對象就是一個還沒有構造完成的對象,這就致使只能看到ThisEscape對象的部分狀態!
看下面示例:我故意讓EventSource對象持有EventListener對象的引用,也意味着:隱式地持有ThisEscape對象的引用了,這就是this引用逸出。
public class EventSource { ThisEscape.EventListener listener;//EventSource對象 持有外部類ThisEscape的 內部類EventListener 的引用 public ThisEscape.EventListener getListener() { return listener; } public void registerListener(ThisEscape.EventListener listener) { this.listener = listener; } }
public class ThisEscapeTest { public static void main(String[] args) { EventSource eventSource = new EventSource(); ThisEscape thisEscape = new ThisEscape(eventSource); ThisEscape.EventListener listener = eventSource.getListener();//this引用逸出 thisEscape.setStringState("change thisEscape state..."); //--------演示一下內存泄漏---------// thisEscape = null;//但願觸發 GC 回收 thisEscape consistentHold(listener);//可是在其餘代碼中長期持有listener引用 } }
額外提一下:內部類對象隱式持有外部類對象,可能會發生內存泄漏問題。
Happens Before 發生在先關係
深入理解這個關係,對判斷代碼中是否存在線程安全性問題頗有幫助。扯一下發生在先關係的前因後果。
爲了加速代碼的執行,底層硬件有寄存器、CPU本地緩存、CPU也有多個核支持多個線程併發執行、還有所謂的指令重排…那如何保證代碼的正確運行?所以Java語言規範要求JVM:
JVM在線程中維護一種相似於串行的語義:只要程序的最終執行結果與在嚴格串行環境中執行的結果相同,那麼寄存器、本地緩存、指令重排都是容許的,從而既保證了計算性能又保證了程序運行的正確性。
在多線程環境中,爲了維護這種串行語義,好比說:操做A發生了,執行操做B的線程如何看到操做A的結果?
Java內存模型(JMM)定義了Happens-Before關係,用來判斷程序執行順序的問題。這個概念仍是太抽象,下面會用具體的示例說明。在我寫代碼的過程當中,發現有四個規則對判斷多線程下程序執行順序很是有幫助:
程序順序規則:
若是程序中操做A在操做B以前(即:寫的代碼語句的順序),那麼在單個線程執行中A操做將在B操做以前執行。
監視器規則:
這個規則是關於鎖的,定義是:在監視器鎖上的解鎖操做必須在同一個監視器鎖上的加鎖操做以前。咋一看,沒啥用。我這裏擴展一下,以下圖:
在線程A內部的全部操做都按照它們在源程序中的前後順序來排序,在線程B內部的操做也是如此。(這就是程序順序規則)
因爲A釋放了鎖,而B得到了鎖,所以A中全部在釋放鎖以前的操做 位於 B中請求鎖以後的全部操做以前。這句話:它的意思就是:在線程A解鎖M以前的全部操做,對於線程B加鎖M以後的全部操做都是可見的。這樣,在線程B中就能看到:線程A對 變量x 、變量y的所寫入的值了。
再擴展一下:爲了在線程之間傳遞數據,咱們常常用到BlockingQueue,一個線程調用put方法添加元素,另外一個線程調用take方法獲取元素,這些操做都知足發生在先關係。線程B不只僅是拿到了一個元素,並且還能看到線程A修改的一些對象的狀態(這就是可見性)
總結一下:
同步操做,好比鎖的釋放和獲取、volatile變量的讀寫,不只知足發生在先關係(偏序),並且還知足全序關係。總之:要想保證執行操做B的線程看到操做A的結果(無論操做A、操做B 是否在同一個線程中執行),操做A、操做B 之間必須知足發生在先關係
volatile變量規則:對volatile變量的寫入操做必須在該變量的讀取操做以前執行。這條規則幫助理解:爲何在聲明類的實例變量時用了volatile修飾,做者的意圖是什麼?
傳遞性:若是操做A在操做B以前執行,操做B在操做C以前執行,那麼操做A必須在操做C以前執行。在你看到一大段代碼,這個線程裏面調用了synchronized修飾的方法、那個線程又向阻塞隊列put了一個元素、另外一個線程又讀取了一個volatile修飾的變量…從這些發生在先規則裏面 使用 傳遞性 就能大體推斷整個代碼的執行流程了。
扯了這麼多,看一個不安全發佈的示例。
public class UnsafeLazyInitialization { private static Resource resource; public static Resource getResource() { if (resource == null) { resource = new Resource();//不安全的發佈 } return resource } }
這段代碼沒有應用到前面提到的任何一個發生在先規則,代碼在執行過程當中發生的指令重排致使了不安全的發佈。
在建立對象、發佈對象時,隱藏了不少操做的。new Resource對象時須要給Resource對象的各個屬性賦值,賦值完了以後,在堆中對象的地址要賦值給 靜態變量resource。在整個過程當中就有可能存在指令重排,看圖:
相似地,雙重檢查加鎖也會致使不安全的發佈。
public class EagerInitialization { private static Resource resource = new Resource(); public static Resource getResource() { return resource; } }
在聲明靜態變量時同時初始化,由JVM來保證初始化過程的安全性。static修飾說明是類變量,於是符合單例模式。
初始化安全性是一種保證:正確構造的對象在沒有同步的狀況下也能安全地在多個線程之間共享,而無論它是如何被髮布的。換句話說:對於被正確構造的對象,全部線程都能看到由構造函數爲對象各個final域設置的正確值。
再換句話說:對於含有final域的對象,初始化安全性能夠防止對象的初始引用被重排序到構造過程以前。這句話已經點破了關鍵了。看上一幅圖,線程A在賦值到半路,太累了,休息了一下,抽了一根菸。而後繼續開始了它的賦值,這些賦值操做,就是對象的構造過程。而在賦值的中間,存在着一個指令重排---將還沒有構造完成的對象的堆地址寫入到初始引用中去了,而若是這個時候剛好有其餘線程拿着這個初始引用去訪問對象(好比訪問該對象的某個屬性),但這個對象還未初始化完成啊,就會致使bug。
哈哈哈哈……是否是仍是看不懂、很抽象?這就是 經。經書級別的經,難唸的經。咱用代碼來講明一下:
public class Resource { private int x;//沒有用final修飾 private String y;//沒有用final修飾 public Resource(int x, String y) { this.x = x; this.y = y; } }
而若是,這兩個屬性都用final修飾的話,那麼就知足初始化安全的保證,就沒有指令重排了。
這就是final關鍵字所起的做用。
另外,你是否是注意到,若是用final修飾實例變量時,IDEA會提示你還沒有給final修飾的實例變量賦初始值?哈哈……
總結一下:
構造函數對final域的全部寫入操做,以及對經過這些域能夠到達的任何變量的寫入操做,都將被「凍結」,而且任何得到該對象引用的線程都至少能確保看到被凍結的值。對於經過final域可到達的初始變量的寫入操做,將不會與構造過程後的操做一塊兒被重排序。
因此:若是Resouce是一個不可變對象,那麼UnsafeLazyInitialization就是安全的了。
//不可變 public class Resource { private final int x; private final String y; public Resource(){x=10;y="hello"} public Resource(int x, String y) { this.x = x; this.y = y; } } //UnsafeLazyInitialization 不只是安全的發佈,並且在多線程訪問中也是線程安全的。 //由於Resource的屬性x、y 都是不可變的。 public class UnsafeLazyInitialization { private static Resource resource; public static Resource getResource() { if (resource == null) { resource = new Resource();//安全的發佈! } return resource; } }
關於初始化安全性,只能保證 final 域修飾的屬性在構造過程完成時的可見性。若是,構造的對象存在非final域修飾的屬性,或者在構造完成後,在程序中其餘地方可以修改屬性的值,那麼必須採用同步來保證可見性(必須採用同步保證線程安全),示例以下:
import java.util.HashMap; import java.util.Map; /** * @author psj * @date 2019/03/10 */ public class UnSafeStates { /** * UnSafeStates 惟一的一個屬性是由final修飾的,初始化安全性仍是存在的 * 即:其餘線程能看到一個正確且 **構造完成** 的UnSafeStates對象 */ private final Map<String,String> states; public UnSafeStates() { states = new HashMap<>(); states.put("hello", "he"); states.put("world", "wo"); } public String getAbbreviation(String s) { return states.get(s); } /** * 這個方法可以修改 states 屬性的值, UnSafeStates 再也不是一個線程安全的類了 * 若是多線程併發調用 setAbbreviation 方法, 就存在線程安全性問題. HashMap的循環引用瞭解一下?哈哈…… * @param key * @param value */ public void setAbbreviation(String key, String value) { states.put(key, value); } }
這個和final關鍵字中討論的初始化安全性相似。只不過,volatile修飾的屬性是知足發生在先關係的。
套用volatile變量規則:在volatile變量的寫入操做必須在對該變量的讀取操做以前執行,那volatile也能避免前面提到的指令重排了。由於,初始化到一半,而後好累,要休息一下,說明初始化過程還沒有完成,也即:變量的寫入操做還沒有完全完成。那根據volatile變量規則:對該變量的訪問也不能開始。這樣就保證了安全發佈。這也是爲何DCL雙重檢查鎖中定義的static變量 用volatile修飾就能安全發佈的緣由。
在寫代碼過程當中,有時不太刻意地去關注安全發佈,在聲明一個類的屬性時,有時就順手給實例變量用一個final修飾。抑或是在考慮多線程訪問到一個狀態變量時,給它用個volatile修飾,並無真正地去思考總結final到底起做用在哪裏了?
因此總結起來就是:final關鍵字在初始化過程當中防止了指令重排,保證了初始化完成後對象的安全發佈。volatile則是經過JMM定義的發生在先關係,保證了變量的內存可見性。
最近在看ES源碼過程當中,看別人寫的代碼,就好奇,哎,爲何這裏這個屬性要用個final呢?爲何那個屬性加了volatile修飾呢?其實只有明白背後原理,才能更好地去理解別人的代碼吧。
固然,上面寫的全是本身的理解,有可能出錯,由於我並無將源代碼編譯成字節碼、甚至是從機器指令角度去分析 上面示例的執行流程,由於我看不懂那些彙編指令,哈哈哈哈哈哈……
《Java併發編程實戰》第3章、第16章
這篇文章前先後後加起來竟然寫了6個小時,沒時間打球了…^:(^ ^:(^
原文:https://www.cnblogs.com/hapjin/p/10505337.html