【多執行緒】如何保證執行緒安全
一、執行緒安全等級
之前的部落格中已有所提及“執行緒安全”問題,一般我們常說某某類是執行緒安全的,某某是非執行緒安全的。其實執行緒安全並不是一個“非黑即白”單項選擇題。按照“執行緒安全”的安全程度由強到弱來排序,我們可以將java語言中各種操作共享的資料分為以下5類:不可變、絕對執行緒安全、相對執行緒安全、執行緒相容和執行緒對立。
1、不可變
在java語言中,不可變的物件一定是執行緒安全的,無論是物件的方法實現還是方法的呼叫者,都不需要再採取任何的執行緒安全保障措施。如final關鍵字修飾的資料不可修改,可靠性最高。
2、絕對執行緒安全
絕對的執行緒安全完全滿足Brian GoetZ給出的執行緒安全的定義,這個定義其實是很嚴格的,一個類要達到“不管執行時環境如何,呼叫者都不需要任何額外的同步措施”通常需要付出很大的代價。
3、相對執行緒安全
相對執行緒安全就是我們通常意義上所講的一個類是“執行緒安全”的。
它需要保證對這個物件單獨的操作是執行緒安全的,我們在呼叫的時候不需要做額外的保障措施,但是對於一些特定順序的連續呼叫,就可能需要在呼叫端使用額外的同步手段來保證呼叫的正確性。
在java語言中,大部分的執行緒安全類都屬於相對執行緒安全的,例如Vector、HashTable、Collections的synchronizedCollection()方法保證的集合。
4、執行緒相容
執行緒相容就是我們通常意義上所講的一個類不是執行緒安全的。
執行緒相容是指物件本身並不是執行緒安全的,但是可以通過在呼叫端正確地使用同步手段來保證物件在併發環境下可以安全地使用。Java API中大部分的類都是屬於執行緒相容的。如與前面的Vector和HashTable相對應的集合類ArrayList和HashMap等。
5、執行緒對立
執行緒對立是指無論呼叫端是否採取了同步錯誤,都無法在多執行緒環境中併發使用的程式碼。由於java語言天生就具有多執行緒特性,執行緒對立這種排斥多執行緒的程式碼是很少出現的。
一個執行緒對立的例子是Thread類的supend()和resume()方法。如果有兩個執行緒同時持有一個執行緒物件,一個嘗試去中斷執行緒,另一個嘗試去恢復執行緒,如果併發進行的話,無論呼叫時是否進行了同步,目標執行緒都有死鎖風險。正因此如此,這兩個方法已經被廢棄啦。
二、執行緒安全的實現方法
保證執行緒安全以是否需要同步手段分類,分為同步方案和無需同步方案。
1、互斥同步
互斥同步是最常見的一種併發正確性保障手段。同步是指在多執行緒併發訪問共享資料時,保證共享資料在同一時刻只被一個執行緒使用(同一時刻,只有一個執行緒在操作共享資料)。而互斥是實現同步的一種手段,臨界區、互斥量和訊號量都是主要的互斥實現方式。因此,在這4個字裡面,互斥是因,同步是果;互斥是方法,同步是目的。
在java中,最基本的互斥同步手段就是synchronized關鍵字,synchronized關鍵字編譯之後,會在同步塊的前後分別形成monitorenter和monitorexit這兩個位元組碼質量,這兩個位元組碼指令都需要一個reference型別的引數來指明要鎖定和解鎖的物件。
此外,ReentrantLock也是通過互斥來實現同步。在基本用法上,ReentrantLock與synchronized很相似,他們都具備一樣的執行緒重入特性。
互斥同步最主要的問題就是進行執行緒阻塞和喚醒所帶來的效能問題,因此這種同步也成為阻塞同步。從處理問題的方式上說,互斥同步屬於一種悲觀的併發策略,總是認為只要不去做正確地同步措施(例如加鎖),那就肯定會出現問題,無論共享資料是否真的會出現競爭,它都要進行加鎖。
2、非阻塞同步
隨著硬體指令集的發展,出現了基於衝突檢測的樂觀併發策略,通俗地說,就是先進行操作,如果沒有其他執行緒爭用共享資料,那操作就成功了;如果共享資料有爭用,產生了衝突,那就再採用其他的補償措施。(最常見的補償錯誤就是不斷地重試,直到成功為止),這種樂觀的併發策略的許多實現都不需要把執行緒掛起,因此這種同步操作稱為非阻塞同步。
非阻塞的實現CAS(compareandswap):CAS指令需要有3個運算元,分別是記憶體地址(在java中理解為變數的記憶體地址,用V表示)、舊的預期值(用A表示)和新值(用B表示)。CAS指令執行時,CAS指令指令時,當且僅當V處的值符合舊預期值A時,處理器用B更新V處的值,否則它就不執行更新,但是無論是否更新了V處的值,都會返回V的舊值,上述的處理過程是一個原子操作。
CAS缺點:
ABA問題:因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。
ABA問題的解決思路就是使用版本號。在變數前面追加版本號,每次變數更新的時候把版本號加一,那麼A-B-A就變成了1A-2B-3C。JDK的atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。
3、無需同步方案
要保證執行緒安全,並不是一定就要進行同步,兩者沒有因果關係。同步只是保證共享資料爭用時的正確性的手段,如果一個方法本來就不涉及共享資料,那它自然就無需任何同步操作去保證正確性,因此會有一些程式碼天生就是執行緒安全的。
1)可重入程式碼
可重入程式碼(ReentrantCode)也稱為純程式碼(Pure Code),可以在程式碼執行的任何時刻中斷它,轉而去執行另外一段程式碼,而在控制權返回後,原來的程式不會出現任何錯誤。所有的可重入程式碼都是執行緒安全的,但是並非所有的執行緒安全的程式碼都是可重入的。
可重入程式碼的特點是不依賴儲存在堆上的資料和公用的系統資源、用到的狀態量都是由引數中傳入、不呼叫 非可重入的方法等。
(類比:synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個執行緒得到一個物件鎖後,再次請求此物件鎖時時可以再次得到該物件的鎖)
2)執行緒本地儲存
如果一段程式碼中所需的資料必須與其他程式碼共享,那就看看這些共享資料的程式碼是否能保證在同一個執行緒中執行?如果能保證,我們就可以把共享資料的可見範圍限制在同一個執行緒之內。這樣無需同步也能保證執行緒之間不出現資料的爭用問題。
符合這種特點的應用並不少見,大部分使用消費佇列的架構模式(如“生產者-消費者”模式)都會將產品的消費過程儘量在一個執行緒中消費完。其中最重要的一個應用例項就是經典的Web互動模型中的“一個請求對應一個伺服器執行緒(Thread-per-Request)”的處理方式,這種處理方式的廣泛應用使得很多Web伺服器應用都可以使用執行緒本地儲存來解決執行緒安全問題。