執行緒安全
執行緒安全
通過這篇部落格你能學到什麼:
編寫執行緒安全的程式碼,本質上就管理 狀態 的訪問,而且通常是 共享的、可變的狀態 .
狀態:可以理解為物件的 成員變數 .
共享: 是指一個變數可以被 多個執行緒 訪問
可變: 是指變數的值在生命週期內 可以改變 .
保證執行緒安全就是要在不可控制的併發訪問中保護資料.
如果物件在多執行緒環境下無法保證執行緒安全,就會導致髒資料和其他不可預期的後果
在多執行緒程式設計中有一個原則:
無論何時,只要有對於一個的執行緒訪問給定的狀態變數,而且其中某個執行緒會 寫入 該變數,此時 必須 使用同步來協調執行緒對該變數的訪問**
Java中使用 synchronized (同步)來確保執行緒安全.在synchronized(同步)塊中的程式碼,可以保證在多執行緒環境下的 原子性 和 可見性 .
不要忽略同步的重要性,如果程式中忽略了必要的同步,可能看上去是可以執行,但是它仍然存在隱患,隨時都可能崩潰.
在沒有正確同步的情況下,如果多執行緒訪問了同一變數( 並且有執行緒會修改變數,如果是隻讀,它還是執行緒安全的 ),你的程式就存在隱患,有三種方法修復它:
1. 不要跨執行緒共享變數
2. 使狀態變為不可變的
3. 在任何訪問狀態變數的時候使用同步
雖然可以用上述三類方法進行修改,但是會很麻煩、困難,所以 一開始就將一個類設計成是執行緒安全的,比在後期重新修復它更容易
封裝可以幫助你構建執行緒安全你的類,訪問特定變數(狀態)的程式碼越少,越容易確保使用恰當的同步,也越容易推斷出訪問一個變數所需的條件.總之, 對程式的狀態封裝得越好,你的程式就越容易實現執行緒安全,同時有助於維護者保持這種執行緒安全性 .
設計執行緒安全的類時,優秀的面向技術--封裝、不可變性(final修飾的)以及明確的不變約束(可以理解為if-else)會給你提供諸多的幫助
雖然程式的響應速度很重要,但是 正確性 才是擺在首位的, 你的程式跑的再快,結果是錯的也沒有任何意義 , 所以要先保證正確性然後再嘗試去優化,這是一個很好的開發原則 .
1 什麼是執行緒安全性
一個類是執行緒安全的,是指在被多個執行緒訪問時,類可以持續進行正確的行為 .
對於執行緒安全類的例項(物件)進行順序或併發的一系列操作,都不會導致例項處於無效狀態.
執行緒安全的類封裝了任何必要的同步,因此客戶不需要自己提供.
2 一個無狀態的(stateless)的servlet
public class StatelessServlet implements Servlet { @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { BigInteger i = extractFromRequest(servletRequest); BigInteger[] factors = factor(i); encodeIntoResponse(servletResponse,factors); } }
我們自定義的StatelessServlet是 無狀態物件 (沒有成員,變數儲存資料),在方法體內宣告的變數i和factors是 本地變數 , 只有進入到這個方法的執行執行緒才能訪問,變數在其他執行緒中不是共享的 , 執行緒訪問無狀態物件的方法,不會影響到其他執行緒訪問該物件時的正確性,所以無狀態物件是執行緒安全的 .
這裡有重要的概念要記好: 無狀態(成員變數)物件永遠是執行緒安全的
3 原子性
在無狀態物件中,加入一個狀態元素,用來計數,在每次訪問物件的方法時執行行自增操作.
public class StatelessServlet implements Servlet { private long count = 0; @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { BigInteger i = extractFromRequest(servletRequest); BigInteger[] factors = factor(i); count++; encodeIntoResponse(servletResponse,factors); }
在單執行緒的環境下執行很perfect,但是 在多執行緒環境下它並不是執行緒安全 的.為什麼呢? 因為 count++;並不是原子操作 ,它是由"讀-改-寫"三個操作組成的,讀取count的值,+1,寫入count的值,我們來想象一下,有兩個執行緒同一時刻都執行到count++這一行,同時讀取到一個數字比如9,都加1,都寫入10,嗯 平白無故少了一計數.
現在我們明白了為什麼自增操作不是執行緒安全的,現在我們來引入一個名詞 競爭條件 .
4 競爭條件
**當計算的正確性依賴於執行時相關的時序或者多執行緒的交替時,會產生競爭條件**.
我對競爭條件的理解就是,**多個執行緒同時訪問一段程式碼,因為順序的問題,可能導致結果不正確,這就是競爭條件**.
除了上面的自增,還有一種常見的競爭條件--"檢查再執行".
廢話不多說,上程式碼.
/** * @author liuboren * @Title: RaceCondition * @ProjectName multithreading * @Description: TODO * @date 2018/10/7 15:54 */ public class RaceCondition { private boolean state = false; public void test(){ if (state){ //做一些事 }else{ // 做另外一些事 } } public void changeState(){ if(state == false){ state = true; }else{ state = false; } } }
程式碼很簡單,test()方法會根據物件的state的狀態執行一些操作,如果state是true就做一些操作,如果是false執行另外一些操作, 在多執行緒條件下,執行緒A剛剛執行test()方法的,執行緒B可能已經改變了狀態值,但其改變後的結果可能對A執行緒不可見,也就是說執行緒A使用的是過期值.這可能導致結果的錯誤 .
5. 示例: 惰性初始化中的競爭條件
這個例子好,多執行緒環境下的單例模式.
/** * @author liuboren * @Title: Singleton * @ProjectName multithreading * @Description: TODO * @date 2018/10/7 16:29 */ public class Singleton { private Singleton singleton; private Singleton() { } public Singleton getSingleton(){ if(singleton ==null){ singleton = new Singleton(); } return singleton; } }
看這個例子,我們把 構造方法宣告為private 的這樣就只能通過getSingleton()來獲得這個物件的例項了,先判斷這個物件是否被例項化了,如果等於null,那就例項化並返回,看似很完美,在單執行緒環境下確實可以正常執行,但是 在多執行緒環境下,有可能兩個執行緒同時走到new物件這一行,這樣就例項化了兩個物件 ,這可能不是我們要的結果,我們來小小修改一下
/** * @author liuboren * @Title: Singleton * @ProjectName multithreading * @Description: TODO * @date 2018/10/7 16:29 */ public class Singleton { private Singleton singleton; private Singleton() { } public Singleton getSingleton(){ if(singleton ==null){ synchronized (this) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
限於篇幅,這裡直接改了一個完美版的, 之所以不在方法宣告 synchronized是為了減少同步快,實現更快的響應 .
6 複合操作
為了避免競爭條件,必須阻止其他執行緒訪問我們正在修改的變數,讓我們可以確保:當其他執行緒想要檢視或修改一個狀態時,必須在我們的執行緒開始之前或者完成之後,而不能在操作過程中
將之前的自增操作改為原子的執行,可以讓它變為執行緒安全的.使用Synchronized(同步)塊,可以讓操作變為原子的.
我們也可以使用原子變數類,是之前的程式碼變為執行緒安全的.
private final AtomicLong count = new AtomicLong(0); @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { BigInteger i = extractFromRequest(servletRequest); BigInteger[] factors = factor(i); count.incrementAndGet(); encodeIntoResponse(servletResponse, factors); }
7 鎖
Java提供關鍵字Synchronized(同步)塊,來保證執行緒安全,可以在多執行緒條件下保證可見性和原子性.
可見性: 一個執行緒修改完物件的狀態後,對其他執行緒可見.
原子性: 可以把複合操作轉換為不可再分的原子操作.一個執行緒執行完原子操作其它執行緒才能執行同樣的原子操作.
讓我們看看另一個關於執行緒安全的結論: 當一個不變約束涉及多個變數時,變數間不是彼此獨立的:某個變數的值會制約其他幾個變數的值.因此,更新一個變數的時候,要在同一原子操作中更新其他幾個 .
覺得過於抽象?我們來看看實際的程式碼
/** * @author liuboren * @Title: StatelessServlet * @ProjectName multithreading * @Description: TODO * @date 2018/10/7 15:04 */ public class StatelessServlet implements Servlet { private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>(); private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>(); @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { BigInteger i = extractFromRequest(servletRequest); if (i.equals(lastNumber.get())) { encodeIntoResponse(servletResponse, lastFactors.get()); } else { BigInteger[] factors = factor(i); lastFactors.set(factors); encodeIntoResponse(servletResponse, lastFactors.get()); /} }
簡單說明一下,AtomicLong是Long和Integer的執行緒安全holder類,AtommicReference是物件引用的執行緒安全holder類. 可以保證他們可以原子的set和get.
我們看一下程式碼,根據lastNumber.get()的結果取返回lastFactors.get()的結果, 這裡存在競爭條件.因為很有可能執行緒A執行完lastNumber.set()且還沒有執行lastFactors.set()的時候,另一個執行緒重新呼叫這個方法進行條件判斷,lastNumber.get()取到了最新值,通過判斷進行響應,但這時響應的lastFactors.get()卻是過期值 !!!!
FBI WARNING : 為了保護狀態的一致性,要在單一的原子操作中更新相互關聯的狀態變數 .
8 內部鎖
每個物件都有一個內部鎖,執行執行緒進入synchronized快之前獲得鎖;而無論通過正常途徑退出,還是從塊中丟擲異常,執行緒在放棄對synchronized塊的控制時自動釋放鎖.獲得內部鎖的唯一途徑是:進入這個內部鎖保護的同步塊或方法.
內部鎖是互斥鎖,意味著至多隻有一個執行緒可以擁有鎖,當執行緒A嘗試請求一個被執行緒B佔有的鎖時,執行緒A必須等待或者阻塞,直到B釋放它,如果B永遠不釋放鎖,A將永遠等待下去
內部鎖對提高執行緒的安全性來說很好,很perfect,but但是,在上鎖的時間段其他執行緒被阻塞了,這會帶來 糟糕的響應性 .
我們再來看之前的單例模式
/** * @author liuboren * @Title: Singleton * @ProjectName multithreading * @Description: TODO * @date 2018/10/7 16:29 */ public class Singleton { private Singleton singleton; private Singleton() { } /*public Singleton getSingleton(){ if(singleton ==null){ synchronized (this) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; }*/ public synchronized Singleton getSingleton() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
在方法上加synchronized可以保證執行緒安全,但是響應性不好,上面註解掉的是之前優化後的方法.
9 用鎖來保護狀態
下面列舉了一些需要加鎖的情況.
1. 操作共享狀態的複合操作必須是原子的,以避免競爭條件 .例如自增和惰性初始化.
2. 並不是所有資料都需要鎖的保護---只有那些被多個執行緒訪問的可變資料 .
3. 對於每一個涉及多個變數的不變約束,需要同一個鎖保護其所有變數
10 活躍度與效能
雖然在方法上宣告 synchronized可以獲得執行緒安全性,但是響應性變得很感人.
限制併發呼叫數量的,並非可用的處理器資源,而恰恰是應用程式自身的結構----我們把這種執行方式描述為弱併發的一種表現.
通過縮小synchronized塊的範圍來維護執行緒安全性,可以很容易提升程式碼的併發性,但是不應該把synchronized塊設定的過小,而且一些很耗時的操作(例如I/O操作)不應該放在同步塊中(容易引發死鎖)
決定synchronized塊的大小需要權衡各種設計要求,包括安全性、簡單性和效能,其中安全性是絕對不能妥協的,而簡單性和效能又是互相影響的(將整個方法宣告為synchronized很簡單,但是效能不太好,將同步塊的程式碼縮小,可能很麻煩,但是效能變好了)
原則:
通常簡單性與效能之間是相互牽制的,實現一個同步策略時,不要過早地為了效能而犧牲簡單性(這是對安全性潛在的妥協).
最後,使用鎖的時候,一些耗時非常長的操作,不要放在鎖裡面,因為執行緒長時間的佔有鎖,就會引起活躍度(死鎖)與效能風險的問題.
嗯,終於寫完了.以上是博主<<Java併發程式設計實戰>>的學習筆記,如果對您有幫助的話,請點下 推薦 , 謝謝 .