《Java併發程式設計實踐》學習筆記之二:執行緒安全性(thread-safe)

技術小胖子發表於2017-11-10

《Java併發程式設計實踐》學習筆記之二:執行緒安全性(thread-safe)

 

1、什麼是執行緒安全性

 
1.1 不可用狀態
 
呼叫一個函式(假設該函式是正確的)操作某物件常常會使該物件暫時陷入不可用的狀態(通常稱為不穩定狀態),等到操作完全結束,該物件才會重新回到完全可用的狀態。
 
1.2 執行緒安全性的核心問題
 
如果其他執行緒企圖訪問一個處於不可用狀態的物件,該物件將不能正確響應從而產生無法預料的結果,如何避免這種情況發生是執行緒安全性的核心問題。
 
單執行緒的程式中是不存在這種問題的,除非有異常發生。
 
1.3 執行緒安全的定義
 
給執行緒安全下定義比較困難。存在很多種定義,如:“一個類在可以被多個執行緒安全呼叫時就是執行緒安全的”。
 
實際上,所有執行緒安全的定義都有某種程式的迴圈,因為它必須符合類的規格說明 ——這是對類的功能、其副作用、哪些狀態是有效和無效的、不可變數、前置條件、後置條件等等的一種非正式的鬆散描述。
 
類要成為執行緒安全的,首先必須在單執行緒環境中有正確的行為。
 
正確性與執行緒安全性之間的關係非常類似於在描述 ACID(原子性、一致性、獨立性和永續性)事務時使用的一致性與獨立性之間的關係:從特定執行緒的角度看,由不同執行緒所執行的物件操作是先後(雖然順序不定)而不是並行執行的。
 
我們都知道,Vector的所有方法都是同步的,然而,儘管如此,在多執行緒環境下有些時候不進行額外的同步仍然是不安全的。
 
考慮下面程式碼:
    Vector v = new Vector(); 
    // contains race conditions — may require external synchronization 
    for (int i=0; i<v.size(); i++) { 
      doSomething(v.get(i)); 
    }
 
如果另一個執行緒恰好在錯誤的時間裡刪除了一個元素,則get()會丟擲一個ArrayIndexOutOfBoundsException。
 
這裡發生的事情是:get(index)的規格說明裡有一條前置條件要求index必須是非負的並且小於size()。但是,在多執行緒環境中,沒有辦法可以知道上一次查到的size()值是否仍然有效,因而不能確定i<size(),除非在上一次呼叫了size()後獨佔地鎖定Vector。
 
更明確地說,這一問題是由 get() 的前置條件是以 size() 的結果來定義的這一事實所帶來的。只要看到這種必須使用一種方法的結果作為另一種講法的輸入條件的樣式,它就是一個狀態依賴,就必須保證至少在呼叫這兩種方法期間元素的狀態沒有改變。一般來說,做到這一點的唯一方法在呼叫第一個方法之前是獨佔性地鎖定物件,一直到呼叫了後一種方法以後。
 

2、Java類的執行緒安全級別

 
Bloch給出的描述五類執行緒安全性的分類方法。
 
2.1 不可變
 
不可變的物件一定是執行緒安全的,並且永遠也不需要額外的同步。因為一個不可變的物件只要構建正確,其外部可見狀態永遠也不會改變,永遠也不會看到它處於不一致的狀態。Java  類庫中大多數基本數值類如 Integer、String和 BigInteger都是不可變的。
 
2.2 執行緒安全
 
由類的規格說明所規定的約束在物件被多個執行緒訪問時仍然有效,不管執行時環境如何排列,執行緒都不需要任何額外的同步。這種執行緒安全性保證是很嚴格的——許多類,如Hashtable或者Vector都不能滿足這種嚴格的定義。
 
2.3 有條件的執行緒安全
 
有條件的執行緒安全類對於單獨的操作可以是執行緒安全的,但是某些操作序列可能需要外部同步。最常見的例子是遍歷由Hashtable或者Vector或者返回的迭代器——由這些類返回的fail-fast迭代器假定在迭代器進行遍歷的時候底層集合不會有變化。為了保證其他執行緒不會在遍歷的時候改變集合,進行迭代的執行緒應該確保它是獨佔性地訪問集合以實現遍歷的完整性。通常,獨佔性的訪問是由對鎖的同步保證的——並且類的文件應該說明是哪個鎖(通常是物件的內部監視器(intrinsic monitor))。
 
2.4 執行緒相容
 
執行緒相容類不是執行緒安全的,但是可以通過正確使用同步而在併發環境中安全地使用。這可能意味著用一個synchronized塊包圍每一個方法呼叫,或者建立一個包裝器物件,其中每一個方法都是同步的(就像Collections.synchronizedList()一樣)。也可能意味著用synchronized塊包圍某些操作序列。
 
常見類:ArrayList、HashMap、SimpleDateFormat、Connection和ResultSet等。
 
2.5 執行緒對立
 
執行緒對立類是那些不管是否呼叫了外部同步都不能在併發使用時安全地呈現的類。執行緒對立很少見,當類修改靜態資料,而靜態資料會影響在其他執行緒中執行的其他類的行為,這時通常會出現執行緒對立。
 

3、記錄執行緒安全級別的好處

 
3.1 記錄執行緒安全
 
通過將類記錄為執行緒安全的(假設它確實是執行緒安全的),您就提供了兩種有價值的服務:您告知類的維護者不要進行會影響其執行緒安全性的修改或者擴充套件,您還告知類的使用者使用它時可以不使用外部同步。
 
3.2 記錄有條件執行緒安全或執行緒相容
 
通過將類記錄為執行緒相容或者有條件執行緒安全的,您就告知了使用者這個類可以通過正確使用同步而安全地在多執行緒中使用。
 
3.3 執行緒對立
 
通過將類記錄為執行緒對立的,您就告知使用者即使使用了外部同步,他們也不能在多執行緒中安全地使用這個類。
 
知道了執行緒安全級別,使用時就可以很好的預防嚴重問題的出現。
 
注意:一個類的執行緒安全行為是其規格說明中的固有部分,應該成為其文件的一部分。因為還沒有描述類的執行緒安全行為的宣告式方式,所以必須用文字描述。
 

4、Servlet的執行緒安全性

 
Servlet/JSP 預設是以多執行緒模式執行的。Servlet 體系結構是建立在 Java 多執行緒機制之上的,它的生命週期是由 Web 容器負責的。當客戶端第一次請求某個 Servlet 時,Servlet  容器將會根據 web.xml 配置檔案例項化這個Servlet 類。當有新的客戶端請求該 Servlet 時,一般不會再例項化該 Servlet 類,也就是有多個執行緒在使用這個例項。Servlet 容器會自動使用執行緒池等技術來支援系統的執行。這樣,當兩個或多個執行緒同時訪問同一個 Servlet時,可能會發生多個執行緒同時訪問同一資源的情況,資料可能會變得不一致。
 
4.1 無狀態Servlet
 
當Servlet不包含域(成員變數),也沒有引用其他類的域,使用的只是區域性變數,而區域性變數是儲存線上程棧中的(各個執行緒有自己一份)。因而無狀態Servlet是執行緒安全的。
 
4.2 有狀態Servlet
 
書中舉了一個例子,接收兩個引數(request中的),計算和(result)。無狀態時,result是區域性變數,現在提升為例項變數。這樣,多使用者訪問時,有可能就會出現自己的結果顯示在別人瀏覽器中的情況。
 
解決這種執行緒不安全性,其中一個主要的方法就是取消 Servlet的例項變數,變成無狀態的Servlet;另外一種方法是對共享資料進行同步操作。使用synchronized關鍵字能保證一次只有一個執行緒可以訪問被保護的區段。
 
執行緒安全問題主要是由例項變數造成的,因此在 Servlet 中應避免使用例項變數。如果應用程式設計無法避免使用例項變數,那麼使用同步來保護要使用的例項變數,但為保證系統的最佳效能,應該同步可用性最小的程式碼。
 

5、補充:Struts1.x與Struts2的執行緒安全性

 
5.1 Struts1.x的執行緒安全性
 
經過對struts1.x原始碼的研讀發現:
struts1.x獲取action的方式是單例的,所有的action都被維護在一個hashMap裡,當有請求到達時,先根據action的名稱去hashMap裡查詢要請求的Action是否已經存在,如果存在,則直接返回hashMap裡的action。如果不存在,則建立一個新的Action例項。這與Servelt是類似的。
 
因而,Action類中不應該宣告帶有狀態的例項變數(與Servlet類似),而應該使用ActionForm,因為ActionForm是通過引數形式傳入action的,不存在共享變數的問題,其實每個request產生的ActionForm例項也是不同的。
 
在Struts1.x與Spring整合時,配置Action的Bean時,scope可以不配,因為預設為“singleton”。經過Polaris測試發現,儘管Struts1.x內部對Action例項的產生是“單例模式”,然而,如果將其交由Spring管理,其例項數量卻是由Spring的scope決定的。可以通過在Action中列印this來測試scope為singleton與prototype時的不同:singleton時,只產生一個例項;為prototype時,每個請求產生產生一個例項。整合的時候,建議Action的Bean不配scope或配成singleton,以利用Struts1自身提供的執行緒模式,以獲得最大效能或資源利用率。
 
在此大概說一下Spring中singleton與prototype的不同:
 
當spring容器中管理bean屬性為singleton時,spring容器會管理該bean整個生命週期;當bean的作用域為prototype時,每次呼叫到該bean都相當於重新new了一次,new出來的物件 如果沒有引用,就會被JVM垃圾回收機制回收的。雖然都說spring是容器,的確沒錯,但是這人為了形象的描述它能帶來的功能,其實它的管理不管理生命週期,其實就看它儲存沒儲存這個物件的引用,雖然singleton是spring管理的,但它在spring容器結束的時候,spring也就是讓這個引用指向一個空物件而已。
 
5.2 Struts2的執行緒安全性
 
 Struts 2 的 Action 物件為每一個請求產生一個例項,因此,雖然在Action中定義了很多全域性變數,也不存線上程安全問題。
 
Struts 2框架在處理每一個使用者請求的時候,都建立一個單獨的執行緒進行處理,值棧ValueStack也是伴隨著區域性執行緒而存在的。在該執行緒存在過程中,可以隨意訪問值棧,這就保證了值棧的安全性。
 
在Struts 2中,ActionContext(資料環境)是一個區域性執行緒,這就意味著每個執行緒中的ActionContext內容都是唯一的。所以開發者不用擔心Action的執行緒安全。
 
在Struts2與Spring整合時,配置Action的Bean時一定記得加上scope屬性,值為:prototype,否則會有執行緒安全問題。
 
5.3 Struts1.x與Struts2的效能問題
 
Struts1.x的單例策略造成了一定的限制,開發時要注意執行緒安全性問題。
 
Struts2是執行緒安全的,據說,Servlet容器會給每一個請求產生許多丟棄的物件,並且不會導致效能和垃圾回收問題。Polaris沒有測試,有興趣的您可以試試。不過,Polaris認為Apache放棄Struts1的更新,轉向Struts2,效能方面應該不會比Struts1差。
 
想了解更多、更詳細的Struts1.x與Struts2的區別,請訪問:http://tech.ddvip.com/2008-12/122852732297849.html
     本文轉自polaris1119 51CTO部落格,原文連結:http://blog.51cto.com/polaris/382161,如需轉載請自行聯絡原作者


相關文章