第三章主要講的共享對象,這章有些內容比較抽象,我理解其中的一些東西費了一些周折。所以把這些理解記錄下來,以免以后遺忘,有些內容是個人的理解,如果您對我的理解有異議,請提出來共同討論。
?
3.1? 可見性
???? 這里提到了“重排序”,指的是操作系統對線程分片后,針對不同線程的調度是沒有特定順序的。
3.1.1 過期數據
???? 貌似沒有什么可說的...
3.1.2 非原子的64位操作
???? 這里指的是對double和long類型64位的變量。對于這種數據編寫多線程程序的時候最好要加volatile標示。因為現在很多cpu是不對64位操作支持的,64位的數據會分成兩個32位的數據,兩部分會分別讀取、操作,這樣就會有兩個存儲數據的地方:內存和cpu緩存。volatile關鍵字指的是強制可見,即內存和cpu緩存數據強制保持一致。這里內存是可以存64位數,但是32位的cpu只能存32位。舉個例子:假設我們有個64位的數,前32位是H 00 00 00 01,后32位是H 00 00 00 10,現在有兩個線程,分別對前32位的數加3和加1,那么最后的結果應該是對前32位加4.列舉如下:
??? 最后的結果:H 00 00 00 05 00 00 00 10
??? 中間結果(加3):H 00 00 00 03 00 00 00 10
????中間結果(加1):H 00 00 00 02 00 00 00 10
??? 按照一般的邏輯,如果沒有同步策略,那么最后看到的結果只有這三種,其實結果是很亂的,很有可能是這種:
????H 00 00 00 01 10 01 11 11 10
????為何會出現這種結果呢?原因在于前面所說的,32位的CPU是把64位的數切開分別做的,那么在低位累加到高位的過程中,會出現重疊,從而造成混亂。
??? 如果不想使用volatile關鍵字,使用鎖策略的話也可以保證64位數的正確操作
3.1.3 鎖和可見性
??? 其實大意在于鎖與可見性是密切想關的,鎖不僅保證了同步與互斥,也保證了內存可見性。
3.1.4 volatile變量
??? 這里主要說了volatile變量是一種輕量級別的鎖,不過說的太粗了,以后的章節會有詳細分析
??? 比較重要的是以下幾點:
??? (1) 主要用于確保對象狀態的可見性,或者標識重要的生命周期的發生
??? (2) volatile的語義不足以使自增操作(++)原子化。這句給了個重要的啟示,那就是說你的算法針對這個變量必須是無狀態的時候才可選用volatile
??? (3)開發和測試階段啟動JVM的時候請開啟 -server選項,這樣JVM會做優化,比如把循環中的沒有改變的判定變量提到循環外面,循環改為無限循環。
????? 比如
?????
volatile boolean asleep: ... while(!asleep)//JVM優化器不會優化該判斷 doSomething();
?
??? 這里加了volatile,所以是不會做優化,因為JVM認為你這是多線程要使用的變量,但是如果沒有加,很可能就會做上述優化,那么這個時候你做多線程,開發階段可能正確,部署階段和生產階段可能就會出問題。
3.2 發布和溢出
??? 這一章比較抽象,以往多線程的書重點在同步和互斥,發布和溢出問題我還是第一次看到這么深入討論的。
??? 先來看看概念:
??? 發布(publishing)一個對象的意思是使它能夠被當前范圍之外的代碼所使用;
??? 逸出(escape):一個對象在尚未準備好時就將它發布,這種清況稱作逸出。
??? 接下來舉了一些逸出的例子:
??? (1)發布到公有區域,一個對象沒有準備好就將一些東西發布到公有區域。
??? (2)從私有區域獲取(允許內部可變的數據逸出)
???
class UnsafeStates{ private String[] states = new String[]{"SK","AL"...} public String[] getStates(){ return states;//直接把private數據發布了出去... } }
??? 這樣完全允許私有對象獲取,違背了其私有性質。 我寫過的代碼好像有過這種情況....
???"發布一個對象,同樣也發布了該對象所有非私有域所引用的對象。更一般的,在一個已經發布的對象中,那些非私有域的引用鏈,和方法調用鏈中的可獲得對象也都會被發布。" 這句話很重要,要結合"非私有域"和"方法調用鏈"來理解。也就是說非私有域是可被調用或者可被覆蓋的,要注意。
?? (3)內部類實例發布。這個發布很抽象,結合代碼來看看
public class ThisEscape{ public ThisEscape(EventSource source){//構造和發布綁定,造成this泄露 source.registerListener( new EventListener(){ public void onEvent(Event e){ doSomething(2); } } ); } }
?
?? 這段代碼是一個很常見的匿名內部類的創建,不過要注意,這里發生的一切是在構造函數中進行的。在構造函數中這么做會有什么問題呢?
?? 問題在于source會調用EventListener,而EventListener是ThisEscape的匿名內部類,它持有對ThisEscape對象實例的引用。source不僅會調用EventListener,實際上source是持有EventListener的引用,那么同時source也就持有了ThisEscape的引用。如果ThisEscape還沒做好實例化的情況下,另外一個線程通過source訪問ThisEscape,這樣就造成了逸出。
???我把上面的代碼理解為"愚蠢的黑社會老大模式",這里有三個角色:老大(ThisEscape),小弟(EventListener),警察(EventSource)。老大犯了事,小弟去頂,這個時候警察要來問小弟問題。 關鍵點就在這里:老大應當知道警察一定也會來問他(EventSource也會找到ThisEscape的實例),但是他卻沒有編好理由去應對(ThisEscape的構造函數還沒有完成)
3.2.1安全的構建實踐
??? 上面的例子指出,一個未完成構造的對象不能被發布出去。
??? 更具體的說,不要讓this引用在構造期間逸出。
??? 比如在構造函數中起線程,會造成this的逸出,這樣this就會被新線程共享,由于this的構造函數還未完成,其他線程會獲取this的不正確的狀態。
???
這里要特別說明的是,上面的意思是:在構造函數中創建線程并沒有問題,但最好不要再構造函數中啟動它。
??? 還有一點,在構造函數中調用一個可覆蓋的實例方法(非private、final)同樣會造成this在構造期間逸出為什么這么說呢?書上沒有給出解釋,但是我簡單想了一下,可以給出如下的例子:
???
public class ThisEscape{ public ThisEscape(EventSource source){ checkSomeThing(); } public void checkSomeThing(){ System.out.println("這個代碼沒有問題,沒有this的逸出"); } }
?這個是你自己的代碼,如果自己用,沒問題,你可以保證你的構造函數引用其他函數的時候沒有包含,但如果你寫的是jdk代碼會怎么樣?使用jdk的客戶端程序員這樣搞一下:
?
?
public class ShitJDK extends ThisEscape{ public void checkSomeThing(){//這里覆蓋了父類的checkSomeThing this.......//這里可以調用this,造成this逸出 } }
?這樣客戶端程序員就很郁悶了。所以作為類庫的開發者首要任務是考慮好封裝,否則問題會很多。
?那么怎么解決上面問題呢?
?首先要分析一下,上面的代碼犯了一個錯誤,就是構造和發布耦合。構造函數是構造的地方,并不是發布的地方,所以,改造的方法就是構造和發布相分離。
?于是我們有三個基本需求:
? (1)構造函數中只包含初始化相關內容。
??(2)至少有兩個步驟,一個是構造,一個是發布。
??(3)客戶端不關心黑老大,也就是說它只需要一個小弟,黑老大封裝(最嚴格的封裝方式是構造函數為private,這樣只能通過本類的靜態方法初始化本類,其他地方根本就不會初始化本類)。
? 書上給出的例子如下:
public class SafeListener{ private final EventListener listener;//老大含有小弟 private SafeListener(){//構造函數私有,并且只包含初始化相關,不包含發布 listener = new EventListener(){ public void onEvent(Event e){ doSomeThing(e); } } } public static SafeListener newInstance(EventSource source){ SafeListener safe = new SafeListener();//這里是調用構造 source.registerListener(safe.listener);//這里是發布,構造與發布相分離 return safe;//這里為什么要return 一個safe?推斷是因為safe還有其他代 //碼,不只一個注冊功能。這個safe從命中初始化開始就和一個listener綁定 } }
?
??? 是不是覺得有點復雜,抽象不好理解?這里是因為除了構造與發布相分離,這里還做了一件事就是封裝了構造與發布相分離的過程。也就是說類的責任沒有完全分開
??? 這里有個很有意思的矛盾點,即:
??? 既要構造與發布分離,又要構造與發布綁定(一起調用)。
??? 第一個點很容易做到,就是分為兩個步驟即可。
??? 第二個點,這里要將兩個步驟合并到一個函數中,以共同調用。 ?第二個點其實可以理解為工廠模式的封裝方法,這里這么封裝是為了SafeListener發布出去的時候就是已經注冊好了的, 這段代碼其實寫的不好,因為它將工廠和工廠生產的東西混在一起了,可以加一個工廠,以更深刻的理解:
???
class SafeListener{//*****本類只負責構造,注意這里沒有了public,是包訪問權限,對內的 private final EventListener listener;//老大含有小弟,注意這里private SafeListener(){//*****構造函數包內可見 listener = new EventListener(){ public void onEvent(Event e){ doSomeThing(e); } } }
EventListener getEventListenerListener(){//*****同樣注意是包訪問權限,對內的
return listener;} ...//其他方法 } public class SafeListenerFactory{//工廠類負責發布任務和封裝任務,和SafeListener在同一個包中,是對外的 public static SafeListener newInstance(EventSource source){ SafeListener safe = new SafeListener();//這里是調用構造 source.registerListener(safe.getEventListenerListener());//這里是發布,構造與發布相分離,注意與上面區分開 return safe;//這里為什么要return 一個safe?推斷是因為safe還有其他代 //碼,不只一個注冊功能。這個safe從命中初始化開始就和一個listener綁定 } }
?? 上面的代碼是不是更好理解了?因為類的責任分開了,更具備面向對象的特征,更好理解。
?? 要注意這里的代碼和上面代碼在訪問權限修飾符上的不同。(*****為訪問權限不同的地方)
?? 上面代碼的類圖如下:
?
?3.3 線程封閉
?線程封閉是指將對象封閉在一個線程當中。這樣就避免了線程共享數據,自動成為線程安全。
?這里舉了兩個例子:
?(1)Swing的線程封閉技術--事件分發線程
? 這里簡單介紹了事件分發線程,講的不是很清楚,有興趣的可以看看這里:
? http://space.itpub.net/13685345/viewspace-374940
?這篇文章對于Swing的事件分發線程作了詳細介紹,主要思想是:不要EDT里面做出了圖形相關的任何事情。
?同時,我很佩服Swing的簡單的設計。
?(2)JDBC應用池
? 這里主要講一個Connection對應一個線程。
3.3.1 Ad-hoc線程限制
?這里不要被Ad-hoc嚇到,Ad-hoc(譯為:非正式的。好抽象....)主要意思如同mashup一樣大眾化,大意是我的類庫不管線程同步問題,所有同步問題交給類庫的使用者解決。這里作者主要要告訴大家不要用Ad-hoc...
3.3.2 棧限制
?這里主要指把變量限制在方法中,即盡量把不需要共享的數據放入方法內部。不過真要共享呢?加鎖同步,或者更簡單的本地線程變量(ThreadLocal)。
3.3.3 ThreadLocal
?這個玩意就是給每個線程一個存儲空間,這樣直接避免了同步問題。但是真要線程共享數據呢?加鎖同步...
?這里介紹了一個ThreadLocal的使用場景,即用一個ThreadLocal變量持有事務上下文,這樣不用在每次函數調用的時候都傳遞這個上下文,是不是很方便?是的,哈哈!
3.4 不可變性
?這里先介紹了不可變對象,先看概念:
? 創建后狀態不能被修改的對象叫做不可變對象。
?其實很好理解,本身不可變意味著其他線程不能改變其數據
3.4.1 Final域
?沒啥,不過這里給出了兩個編程的最佳實踐:
?將所有的域聲明為私有,除非它們具有更高的可見性;
?將所有的域聲明為final,除非它們是可變的。
3.4.2 示例 : 使用vloatile發布不可變對象
?這里主要給出了兩個代碼,第二個代碼使用第一個類的時候加了vloatile修飾符,主要因為第一個類為不可變對象,不需要同步,但是有可見性問題,所以一定要加volatile,以保證兩個變量同時可見。代碼就不貼了,感興趣的可以看書。
?
3.5 安全發布
?這里分了幾小節,把里面的好的思想提出來如下:
?(1)final總是可以安全的用于任意線程(除了容器類)
?(2)安全發布的類型:
- 通過靜態初始化器初始化對象的引用(上面的黑老大例子)或者最簡單的:
?????????? public static Holder holder = new Holder();//這里靜態初始化器由JVM在類的初始階段執行,也就是說客戶
???????????????????????????????????????????????????????????????????????????? //寫的線程目前還都沒有啟動。
- 將它的引用存儲到volatile域或者AtomicReference
- 將它的引用存儲到正確創建對象的final域中
- 將它的引用存儲到由鎖正確保護的域中
?(3)高效不可變對象
??????? 概念:一個對象在技術上是不可變的,但是它的狀態不會再發布后改變。
?????? ? 從這里看,與其叫"高效不可變對象",不如叫"偽不可變對象"更合適....
?
?(4)總結了發布對象的必要條件依賴于對象的可變性:
- 不可變對象可以通過任意機制發布
- 高效不可變對象必須要安全發布
- 可變對象必須要安全發布,同時必須要線程安全或者被鎖保護
?(5)使用和共享對象的一些最有效的策略:
- 線程限制
- 共享只讀
- 共享線程安全:有自己的同步代碼,客戶端代碼無需寫額外同步代碼
- 被守護的:即加鎖訪問。最一般的措施...
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
