在構建穩健的併發程序時,必須正確地使用線程和鎖。但這些終歸只是一些機制。要編寫線程安全的代碼,其核心在於要對訪問狀態操做進行管理,特別是對共享的(Shared)和可變的(Mutable)狀態的訪問。java
從非正式的意義上來講,對象的狀態是指存儲在狀態變量中的數據。對象的狀態可能包括其餘依賴的對象的域。例如,某個HashMap的狀態不只存儲在HashMap對象自己,還存儲在許多Map.Entry對象中。在對象的狀態中包含了任何可能影響其外部可見行爲的數據。web
「共享」意味着變量能夠由多個線程同時訪問,而「可變」則意味着變量的值在其生命週期內能夠發生變化。一個對象是否須要是線程安全的,取決於它是否被多個線程訪問。這指的是在程序中訪問對象的方式,而不是對象要實現的功能。要使得對象是線程安全的,須要採用同步機制來協同對對象可變狀態的訪問。若是沒法實現協同,那麼可能會致使數據破壞以及其餘不應出現的結果。編程
當多個線程訪問某個狀態變量而且其中有一個線程執行寫入操做時,必須採用同步機制來協同這些線程對變量的訪問。Java中的主要同步機制是關鍵字synchronized,它提供了一種獨佔的加鎖方式,但「同步」這個術語還包括volatile類型的變量,顯示鎖(Explicit Lock)以及原子變量。安全
若是當多個線程訪問同一個可變的狀態變量時沒有使用合適的同步,那麼程序就會出現錯誤。有三種方式能夠修復這個問題:網絡
- 不在線程之間共享該狀態變量;
- 將狀態變量修改成不可變的變量;
- 在訪問狀態變量時使用同步;
若是在設計類時沒有考慮併發訪問的狀況,那麼在採用上述方法時可能須要對設計進行重大修改,所以要修復這個問題可謂是知易行難。若是從一開始就設計一個線程安全的類,那麼比在之後再將這個類修改成線程安全的類要容易的多。併發
當設計線程安全的類時,良好的面向對象技術、不可修改性,以及明晰的不變性規範都能起到必定的幫助做用。less
當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些線程將如何交替執行,而且在主調代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。性能
在線程安全類中封裝了必要的同步機制,所以客戶端無需進一步採用同步措施。atom
來看一個實例:一個無狀態的類spa
public class StatelessFactorizer { public void service(int i) { } }
它既不包含任何域,也不包含任何對其餘類中域的引用。訪問StatelessFactorizer的線程不會影響到另外一個訪問同一個StatelessFactorizer的線程的計算結果。
無狀態對象必定是線程安全的。
在web開發中,大多數Servlet都是無狀態的,從而極大地下降了在實現Servlet線程安全性時的複雜性。
當咱們在無狀態對象中增長一個狀態時,會出現什麼狀況呢?假設咱們但願增長一個「命中計數器(Hit Counter)」來統計所處理的請求數量。一種直觀的方式是在類中增長一個long類型的域,而且每處理一個請求就將這個值加1,例如:
// 線程不安全 public class StatelessFactorizer { private long count = 0; public long getCount() { return count; } public void service(int i) { ++count; } }
很不幸,雖然遞增操做++count是一宗緊湊的語法,使其看上去只是一個操做,但這個操做並不是原子性的,於是它並不會做爲一個不可分割的操做來執行。實際上,它包含了三個獨立的操做:讀取count的值,將值+1,而後將計算結果寫入count中。這個一個「讀取——修改——寫入」的操做序列,而且其結果狀態依賴於以前的狀態。
在併發編程中,這種因爲不恰當的執行時序而出現不正確的結果是一種很是重要的狀況,他有一個正式的名字:競態條件(Race Condition)。
當存在多個競態條件,會使結果變得不可靠。當某個計算的正確性取決於多個線程的交替執行時序時,那麼就會發生 競態條件。換句話說,就是結果取決與運氣。最多見的競態條件類型就是「先檢查後執行(Check-Then-Act)」操做,即經過一個可能失效的觀測結果來決定下一步的動做。
使用「先檢查後執行」的一種常見狀況就是延遲初始化。延遲初始化的目的是將對象的初始化操做延遲到實際被使用時才進行,同事要確保只初始化一次。例如:
// 線程不安全 public class LazyInitRace { private LazyInitRace instance = null; public LazyInitRace getInstance() { if (instance == null) instance = new LazyInitRace(); return instance; } }
在LazyInitRace中包含了一個競態條件,它可能會破壞這個類的正確性。假定線程A和線程B同時執行getInstance。A看到instance爲空,於是建立一個新的實例。B一樣須要判斷instance是否爲空。此時的instance是否爲空,取決於不可預測的時序,包括線程的調度方式,以及A須要花多長時間來初始化實例並設置instance。若是當B檢查時,instance爲空,那麼在兩次調用getInstance時可能會獲得不一樣的結果,即便getInstance一般被認爲是返回相同的實例。
LazyInitRace和StatelessFactorizer都包含一組須要以原子的方式執行的操做。要避免競態條件問題,就必須在某個線程修改該變量時,經過某種方式阻止其餘線程使用這個變量,從而確保其餘線程只能在修改操做完成以前或以後讀取和修改狀態,而不是在修改狀態的過程當中。
在StatelessFactorize中的計數器問題,咱們可使用加鎖機制,在接下會介紹,這是Java中用於確保原子性的內置機制。就目前而言,咱們先採用另外一種方式來修復這個問題,即便用一個現有的線程安全類:
package cn.net.bysoft.lesson2; import java.util.concurrent.atomic.AtomicLong; // 線程安全 public class StatelessFactorizer { private final AtomicLong count = new AtomicLong(0); public long getCount() { return count.get(); } public void service(int i) { count.incrementAndGet(); } }
在實際狀況中,應儘量地使用現有的線程安全對象來管理類的狀態。與非線程安全的對象相比,判斷線程安全對象的可能狀態及其狀態轉換狀況要更爲容易,從而也更容易維護和驗證線程安全性。
當在StatelessFactorizer中添加一個狀態變量時,能夠經過線程安全的對象來管理Servlet的狀態以維護安全性。但若是想在其中添加更多的狀態,那麼是否只需添加更多的線程安全狀態變量就足夠了?
要保持狀態的一致性,就須要在單個原子操做中更新全部相關的狀態變量。
Java提供了一種內置鎖來支持原子性:同步代碼塊(Synchronized Block)。同步代碼塊包括兩部分:一個做爲鎖的對象引用,一個做爲由這個鎖保護的代碼塊。以關鍵字synchronized來修飾的方法就是一種橫跨整個方法體的同步塊,其中該同步塊的鎖就是方法調用所在的對象。靜態的synchronized方法以Class對象做爲鎖。
每一個Java對象均可以用作一個實現同步的鎖,這些鎖被稱爲內置鎖(Intrinsic Lock)或監視鎖(Monitor Lock)。線程在進入同步代碼塊以前會自動得到鎖,而且在退出同步代碼塊時自動釋放鎖,而不管是經過正常的控制路徑退出,仍是經過從代碼塊中拋出異常退出。得到內置鎖的惟一途徑就是進入由這個鎖保護的代碼塊或方法。
當某個線程請求一個由其餘線程持有的鎖時,發出請求的線程就會阻塞。而後,因爲內置鎖是可重入的,所以若是某個線程試圖得到一個已經由本身持有的鎖,那麼這個請求就會成功。「重入」意味着獲取鎖的操做的顆粒是「線程」,而不是「調用」。重入的一種實現方法是,爲每一個鎖關聯一個獲取計數值和一個全部者線程。當計數值爲0時,這個鎖就被認爲是沒有被任何線程持有。當線程請求一個未被持有的鎖時,JVM將記下鎖的持有者,而且將獲取計數值設置爲1.若是同一個線程再次獲取這個鎖,計數值將遞增,而當線程退出同步代碼塊時,計數器會相應地遞減。當計數值爲0時,這個鎖被釋放。
因爲鎖能使其保護的代碼路徑以串行形式來訪問,所以能夠經過鎖來構造一些協議以實現對共享狀態的獨佔訪問。只要始終遵循這些協議,就能確保狀態的一致性。
對於可能被多個線程同時訪問的可變狀態變量,在訪問它時都須要持有一個鎖,在這種狀況下,咱們稱狀態變量是由這個鎖保護的。
對象的內置鎖與其狀態之間沒有內在的關聯。雖然大多數類都將內置鎖用做一種有效的加鎖機制,但對象的域並不必定要經過內置鎖來保護。當獲取與對象關聯的鎖時,並不能阻止其餘線程訪問該對象,某個線程在得到對象的鎖以後,只能阻止其餘線程得到同一個鎖。之因此每一個對象都有一個內置鎖,只是爲了免去顯示地建立鎖對象。你須要自行構造加鎖協議或者同步策略來實現對共享狀態的安全訪問,而且在程序中自始至終地使用他們。
每一個共享的和可變的變量都應該只由一個鎖來保護,從而使維護人員知道是哪個鎖。
當某個變量由鎖來保護時,意味着在每次訪問這個變量時都須要首先得到鎖,這樣就確保在同一時刻只有一個線程能夠訪問這個變量。當類的不變性條件涉及多個狀態變量時,那麼還有另一個需求:在不變性條件中的每一個變量都必須由同一個鎖來保護。
對於每一個包含多個變量的不變性條件,起重涉及的全部變量都須要由同一個鎖來保護。
有時候將一個方法用synchronized修飾得到鎖,性能方面會很是糟糕。由於每次只能有一個線程在執行這個方法。若是這個方法須要執行很長時間,那麼其餘的線程必須一直等待這個線程執行完。
在設計時要判斷同步代碼塊的合理大小,須要在各類需求之間權衡,包括安全性、簡單性和性能。有時候,在簡單性和性能之間會發生衝突,但在兩者之間一般能找到某種合理的平衡。
一般,在簡單性與性能之間存在着相互制約因素。當實現某個同步策略時,必定不要盲目地爲了性能而犧牲簡單性(這可能會破壞安全性)。
當使用鎖時,你應該清除代碼塊中實現的功能,以及在執行該代碼塊時是否須要很長的時間。不管是執行計算密集的操做,仍是在執行某個可能阻塞的操做,若是持有鎖的時間過長,那麼都會帶來活躍性或性能問題。
當執行時間較長的計算或者可能沒法快速完成的操做時(例如,網絡I/O或控制檯I/O),必定不要持有鎖。