谈谈JAVA中的安全发布
談?wù)凧AVA中的安全發(fā)布
昨天看到一篇文章闡述技術(shù)類資料的"等級",看完之后很有共鳴。再加上最近在工作中越發(fā)覺得線程安全性的重要性和難以捉摸,又掏出了《Java并發(fā)編程實戰(zhàn)》研讀一番,這本書應(yīng)該是屬于為“JAVA 多線程作注解”的一本書,那我就為書中關(guān)于對象安全發(fā)布的內(nèi)容作一些注解,作為今年的第一篇博文。
我讀的是中文版,確實感覺書中有些地方的描述晦澀難懂,也沒有去拿英文原文來對照,就按中文版描述,再配上一些示例代碼記錄我的一些理解吧。
1. 安全發(fā)布的定義
發(fā)布是個動詞,是去發(fā)布對象。而對象,通俗的理解是:JAVA里面通過 new 關(guān)鍵字 創(chuàng)建一個對象。
發(fā)布一個對象的意思是:使對象在當(dāng)前作用域之外的代碼中使用。比如下面knowSecrets指向的HashSet類型的對象,由static修飾,是一個類變量。當(dāng)前作用域為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,導(dǎo)致 在其他類中也能訪問到這個HashSet對象,比如向HashSet添加元素或者刪除元素。因此,也就發(fā)布了這個對象。
public class UsingSecret {public static void main(String[] args) {PublishExample.knowSecrets.add(new Secret());PublishExample.knowSecrets.remove(new Secret());} }另外,值得注意的是:添加到HashSet集合中的Secret對象也被發(fā)布了。
2. 不安全的發(fā)布
因為對象一般是在構(gòu)造函數(shù)里面初始化的(不討論反射),當(dāng) new 一個對象時,會為這個對象的屬性賦值,當(dāng)前時刻對象各個屬性擁有的值 稱為對象的狀態(tài)。
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對象的當(dāng)前狀態(tài)Secret secObjCurrentState = new Secret("current state", 5);//改變 secObjCurrentState 對象的狀態(tài)secObjCurrentState.setPassword("state changed");}public void setPassword(String password) {this.password = password;} }Secret對象有兩個屬性:password和length,secObjCurrentState.setPassword("state changed")改變了對象的狀態(tài)。
創(chuàng)建對象的目的是使用它,而要用它,就要把它發(fā)布出去。同時,也引出了一個重要問題,我們是在哪些地方用到這個對象呢?比如:只在一個線程里面訪問這個對象,還是有可能多個線程并發(fā)訪問該對象?
對象被發(fā)布后,是無法知道其他線程對已發(fā)布的對象執(zhí)行何種操作的,這也是導(dǎo)致線程安全問題的原因。
2.1 this引用逸出
先看一個不安全發(fā)布的示例----this引用逸出。參考《Java并發(fā)編程實戰(zhàn)》第3章程序清單3-7
當(dāng)我第一次看到"this引用逸出"時,是懵逼的。后來在理解了“發(fā)生在先”原則、“初始化過程安全性”、"volatile關(guān)鍵字的作用"之后才慢慢理解了。這些東西后面再說。
外部類ThisEscape和它的內(nèi)部類EventListener
public class ThisEscape {private int intState;//外部類的屬性,當(dāng)構(gòu)造一個外部類對象時,這些屬性值就是外部類狀態(tài)的一部分private String stringState;public ThisEscape(EventSource source) {source.registerListener(new EventListener(){@Overridepublic void onEvent(Event e) {doSomething(e);}}); //執(zhí)行到這里時,new 的EventListener就已經(jīng)把ThisEscape對象隱式發(fā)布了,而ThisEscape對象尚未初始化完成intState=10;//ThisEscape對象繼續(xù)初始化....stringState = "hello";//ThisEscape對象繼續(xù)初始化....//執(zhí)行到這里時, ThisEscape對象才算初始化完成...}/*** EventListener 是 ThisEscape的 非靜態(tài) 內(nèi)部類*/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;}現(xiàn)在要創(chuàng)建一個ThisEscape對象,于是執(zhí)行ThisEscape的構(gòu)造方法,構(gòu)造方法里面有 new EventListener對象,于是EventListener對象就隱式地持有外部類ThisEscape對象的引用。
那如果能在其他地方訪問到EventListner對象,就意味著"隱式"地發(fā)布了ThisEscape對象,而此時ThisEscape對象可能還尚未初始化完成,因此ThisEscape對象就是一個尚未構(gòu)造完成的對象,這就導(dǎo)致只能看到ThisEscape對象的部分狀態(tài)!
看下面示例:我故意讓EventSource對象持有EventListener對象的引用,也意味著:隱式地持有ThisEscape對象的引用了,這就是this引用逸出。
public class EventSource {ThisEscape.EventListener listener;//EventSource對象 持有外部類ThisEscape的 內(nèi)部類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...");//--------演示一下內(nèi)存泄漏---------//thisEscape = null;//希望觸發(fā) GC 回收 thisEscapeconsistentHold(listener);//但是在其他代碼中長期持有l(wèi)istener引用} }額外提一下:內(nèi)部類對象隱式持有外部類對象,可能會發(fā)生內(nèi)存泄漏問題。
2.2 不安全的延遲初始化
Happens Before 發(fā)生在先關(guān)系
深刻理解這個關(guān)系,對判斷代碼中是否存在線程安全性問題很有幫助。扯一下發(fā)生在先關(guān)系的來龍去脈。
為了加速代碼的執(zhí)行,底層硬件有寄存器、CPU本地緩存、CPU也有多個核支持多個線程并發(fā)執(zhí)行、還有所謂的指令重排…那如何保證代碼的正確運行?因此Java語言規(guī)范要求JVM:
JVM在線程中維護一種類似于串行的語義:只要程序的最終執(zhí)行結(jié)果與在嚴(yán)格串行環(huán)境中執(zhí)行的結(jié)果相同,那么寄存器、本地緩存、指令重排都是允許的,從而既保證了計算性能又保證了程序運行的正確性。
在多線程環(huán)境中,為了維護這種串行語義,比如說:操作A發(fā)生了,執(zhí)行操作B的線程如何看到操作A的結(jié)果?
Java內(nèi)存模型(JMM)定義了Happens-Before關(guān)系,用來判斷程序執(zhí)行順序的問題。這個概念還是太抽象,下面會用具體的示例說明。在我寫代碼的過程中,發(fā)現(xiàn)有四個規(guī)則對判斷多線程下程序執(zhí)行順序非常有幫助:
程序順序規(guī)則:
如果程序中操作A在操作B之前(即:寫的代碼語句的順序),那么在單個線程執(zhí)行中A操作將在B操作之前執(zhí)行。
監(jiān)視器規(guī)則:
這個規(guī)則是關(guān)于鎖的,定義是:在監(jiān)視器鎖上的解鎖操作必須在同一個監(jiān)視器鎖上的加鎖操作之前。咋一看,沒啥用。我這里擴展一下,如下圖:
在線程A內(nèi)部的所有操作都按照它們在源程序中的先后順序來排序,在線程B內(nèi)部的操作也是如此。(這就是程序順序規(guī)則)
由于A釋放了鎖,而B獲得了鎖,因此A中所有在釋放鎖之前的操作 位于 B中請求鎖之后的所有操作之前。這句話:它的意思就是:在線程A解鎖M之前的所有操作,對于線程B加鎖M之后的所有操作都是可見的。這樣,在線程B中就能看到:線程A對 變量x 、變量y的所寫入的值了。
再擴展一下:為了在線程之間傳遞數(shù)據(jù),我們經(jīng)常用到BlockingQueue,一個線程調(diào)用put方法添加元素,另一個線程調(diào)用take方法獲取元素,這些操作都滿足發(fā)生在先關(guān)系。線程B不僅僅是拿到了一個元素,而且還能看到線程A修改的一些對象的狀態(tài)(這就是可見性)
總結(jié)一下:
同步操作,比如鎖的釋放和獲取、volatile變量的讀寫,不僅滿足發(fā)生在先關(guān)系(偏序),而且還滿足全序關(guān)系。總之:要想保證執(zhí)行操作B的線程看到操作A的結(jié)果(不管操作A、操作B 是否在同一個線程中執(zhí)行),操作A、操作B 之間必須滿足發(fā)生在先關(guān)系
volatile變量規(guī)則:對volatile變量的寫入操作必須在該變量的讀取操作之前執(zhí)行。這條規(guī)則幫助理解:為什么在聲明類的實例變量時用了volatile修飾,作者的意圖是什么?
傳遞性:如果操作A在操作B之前執(zhí)行,操作B在操作C之前執(zhí)行,那么操作A必須在操作C之前執(zhí)行。在你看到一大段代碼,這個線程里面調(diào)用了synchronized修飾的方法、那個線程又向阻塞隊列put了一個元素、另一個線程又讀取了一個volatile修飾的變量…從這些發(fā)生在先規(guī)則里面 使用 傳遞性 就能大致推斷整個代碼的執(zhí)行流程了。
扯了這么多,看一個不安全發(fā)布的示例。
public class UnsafeLazyInitialization {private static Resource resource;public static Resource getResource() {if (resource == null) {resource = new Resource();//不安全的發(fā)布}return resource} }這段代碼沒有應(yīng)用到前面提到的任何一個發(fā)生在先規(guī)則,代碼在執(zhí)行過程中發(fā)生的指令重排導(dǎo)致了不安全的發(fā)布。
在創(chuàng)建對象、發(fā)布對象時,隱藏了很多操作的。new Resource對象時需要給Resource對象的各個屬性賦值,賦值完了之后,在堆中對象的地址要賦值給 靜態(tài)變量resource。在整個過程中就有可能存在指令重排,看圖:
類似地,雙重檢查加鎖也會導(dǎo)致不安全的發(fā)布。
3. 安全的發(fā)布
public class EagerInitialization {private static Resource resource = new Resource();public static Resource getResource() {return resource;} }在聲明靜態(tài)變量時同時初始化,由JVM來保證初始化過程的安全性。static修飾說明是類變量,因而符合單例模式。
3.1 初始化安全性
初始化安全性是一種保證:正確構(gòu)造的對象在沒有同步的情況下也能安全地在多個線程之間共享,而不管它是如何被發(fā)布的。換句話說:對于被正確構(gòu)造的對象,所有線程都能看到由構(gòu)造函數(shù)為對象各個final域設(shè)置的正確值。
再換句話說:對于含有final域的對象,初始化安全性可以防止對象的初始引用被重排序到構(gòu)造過程之前。這句話已經(jīng)點破了關(guān)鍵了。看上一幅圖,線程A在賦值到半路,太累了,休息了一下,抽了一根煙。然后繼續(xù)開始了它的賦值,這些賦值操作,就是對象的構(gòu)造過程。而在賦值的中間,存在著一個指令重排---將尚未構(gòu)造完成的對象的堆地址寫入到初始引用中去了,而如果這個時候恰好有其他線程拿著這個初始引用去訪問對象(比如訪問該對象的某個屬性),但這個對象還未初始化完成啊,就會導(dǎo)致bug。
哈哈哈哈……是不是還是看不懂、很抽象?這就是 經(jīng)。經(jīng)書級別的經(jīng),難念的經(jīng)。咱用代碼來說明一下:
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關(guān)鍵字所起的作用。
另外,你是不是注意到,如果用final修飾實例變量時,IDEA會提示你尚未給final修飾的實例變量賦初始值?哈哈……
總結(jié)一下:
構(gòu)造函數(shù)對final域的所有寫入操作,以及對通過這些域可以到達的任何變量的寫入操作,都將被“凍結(jié)”,并且任何獲得該對象引用的線程都至少能確保看到被凍結(jié)的值。對于通過final域可到達的初始變量的寫入操作,將不會與構(gòu)造過程后的操作一起被重排序。
所以:如果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 不僅是安全的發(fā)布,而且在多線程訪問中也是線程安全的。 //因為Resource的屬性x、y 都是不可變的。 public class UnsafeLazyInitialization {private static Resource resource;public static Resource getResource() {if (resource == null) {resource = new Resource();//安全的發(fā)布!}return resource;} }關(guān)于初始化安全性,只能保證 final 域修飾的屬性在構(gòu)造過程完成時的可見性。如果,構(gòu)造的對象存在非final域修飾的屬性,或者在構(gòu)造完成后,在程序中其他地方能夠修改屬性的值,那么必須采用同步來保證可見性(必須采用同步保證線程安全),示例如下:
import java.util.HashMap; import java.util.Map; /*** @author psj* @date 2019/03/10*/ public class UnSafeStates {/*** UnSafeStates 唯一的一個屬性是由final修飾的,初始化安全性還是存在的* 即:其他線程能看到一個正確且 **構(gòu)造完成** 的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 不再是一個線程安全的類了* 如果多線程并發(fā)調(diào)用 setAbbreviation 方法, 就存在線程安全性問題. HashMap的循環(huán)引用了解一下?哈哈……* @param key* @param value*/public void setAbbreviation(String key, String value) {states.put(key, value);} }3.2 volatile 修飾的屬性的安全發(fā)布問題
這個和final關(guān)鍵字中討論的初始化安全性類似。只不過,volatile修飾的屬性是滿足發(fā)生在先關(guān)系的。
套用volatile變量規(guī)則:在volatile變量的寫入操作必須在對該變量的讀取操作之前執(zhí)行,那volatile也能避免前面提到的指令重排了。因為,初始化到一半,然后好累,要休息一下,說明初始化過程尚未完成,也即:變量的寫入操作尚未徹底完成。那根據(jù)volatile變量規(guī)則:對該變量的訪問也不能開始。這樣就保證了安全發(fā)布。這也是為什么DCL雙重檢查鎖中定義的static變量 用volatile修飾就能安全發(fā)布的原因。
4. 總結(jié)
在寫代碼過程中,有時不太刻意地去關(guān)注安全發(fā)布,在聲明一個類的屬性時,有時就順手給實例變量用一個final修飾。抑或是在考慮多線程訪問到一個狀態(tài)變量時,給它用個volatile修飾,并沒有真正地去思考總結(jié)final到底起作用在哪里了?
所以總結(jié)起來就是:final關(guān)鍵字在初始化過程中防止了指令重排,保證了初始化完成后對象的安全發(fā)布。volatile則是通過JMM定義的發(fā)生在先關(guān)系,保證了變量的內(nèi)存可見性。
最近在看ES源碼過程中,看別人寫的代碼,就好奇,哎,為什么這里這個屬性要用個final呢?為什么那個屬性加了volatile修飾呢?其實只有明白背后原理,才能更好地去理解別人的代碼吧。
當(dāng)然,上面寫的全是自己的理解,有可能出錯,因為我并沒有將源代碼編譯成字節(jié)碼、甚至是從機器指令角度去分析 上面示例的執(zhí)行流程,因為我看不懂那些匯編指令,哈哈哈哈哈哈……
5. 參考資料
《Java并發(fā)編程實戰(zhàn)》第3章、第16章
這篇文章前前后后加起來居然寫了6個小時,沒時間打球了…^:(^ ^:(^
原文:https://www.cnblogs.com/hapjin/p/10505337.html
轉(zhuǎn)載于:https://www.cnblogs.com/hapjin/p/10505337.html
總結(jié)
以上是生活随笔為你收集整理的谈谈JAVA中的安全发布的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 文件无法复制到c盘的解决办法
- 下一篇: Xcode 9有什么新功能?