1 線程安全定義 含糊的定義:如果一個對象可以安全地被多個線程同時使用,那它就是線程安全的 嚴謹的定義: 當多個線程同時訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行為都可以獲得正確的結果,那就稱這個對 ...
目錄
1 線程安全定義
含糊的定義:如果一個對象可以安全地被多個線程同時使用,那它就是線程安全的
嚴謹的定義:
當多個線程同時訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行為都可以獲得正確的結果,那就稱這個對象是線程安全的。-----From《Java併發編程實戰》作者Brian Goetz
2 Java數據與線程安全
從線程安全形度,將Java中各種操作共用的數據分為:不可變、絕對線程安全、相對線程安全、線程相容和線程對立
2.1 不可變
不可變的對象一定是線程安全。
如何保證不可變呢?
- 基本數據類型,用final修飾
- 對象類型:用final修飾對象中可變的欄位
Java中常用的不可變對象:String、Number的部分子類如 Long、Double、BigInteger、BigDecimal等。
為什麼String不可變,參考:基本數據類型及String
2.2 絕對線程安全
ConcurrentHashMap (我後續會更新ConcurrentHashMap源碼分析專題)
2.3 相對線程安全
定義:只能保證對象單次的操作是線程安全,連續調用不能保證線程安全
大部分聲稱線程安全的類都屬於這種類型,例如Vector、HashTable、Collections的 synchronizedCollection()方法包裝的集合等。
2.4 線程相容
定義:對象本身並不是線程安全的,但是可以通過在調用端使用同步手段來保證對象在併發環境中可以安全地使用。如集合類ArrayList和HashMap等。
2.5 線程對立
定義:不管調用端是否採取了同步措施,都無法在多線程環境中併發使用代碼。
例子:Thread類的suspend()和resume()方法:如果有兩個線程同時持有一個線程對象,一個嘗試去中斷線程,一個嘗試去恢複線程,在併發進行的情況下,無論調用時是否進行了同步,目標線程都存在死鎖風險
3 Java線程安全支持
JVM同步機制和鎖類庫實現線程安全
3.1 互斥同步
同步:在多個線程併發訪問共用數據時,保證共用數據在同一個時刻只被一條線程使用
互斥:實現同步的一種手段。
互斥同步性能開銷:互斥同步屬於一種悲觀的併發策略,無論共用的數據是否真的會出現競爭,它都會進行加鎖,引發:用戶態到核心態轉換、維護鎖計數器、檢查是否有被阻塞的線程需要被喚醒 等開銷
3.1.1 synchronized互斥同步原理
- synchronized關鍵字經過Javac編譯之後,會在同步塊的前後分別形成monitorenter和monitorexit這兩個位元組碼指令。
- 執行monitorenter指令時,首先要去嘗試獲取對象的鎖。如果這個對象沒被鎖定,或者當前線程已經持有了那個對象的鎖,就把鎖的計數器的值增加1
- 執行 monitorexit指令時會將鎖計數器的值減1,一旦計數器的值為零,鎖隨即就被釋放
- 如果獲取對象鎖失敗,那當前線程阻塞等待,直到鎖被釋放。
關於同步塊的說明:
monitorenter和monitorexit指令,都需要一個reference類型的參數,指明要鎖定和解鎖的對象。synchronized修飾地方不同,reference取不同的值:
- 修飾 對象,取這個對象的引用作為reference;
- 修飾 實例方法,取方法所屬對象實例作為reference,
- 修飾 類方法,取Class對象來作為線程要持有的鎖
根據兩個monitorenter和monitorexit這兩個位元組碼指令執行過程,可以得出以下推論:
- 被synchronized修飾的同步塊對同一條線程來說是可重入的
- 被synchronized修飾的同步塊在持有鎖的線程執行完畢並釋放鎖之前,會無條件地阻塞後面其他線程的進入
3.1.2 Lock介面和ReentrantLock互斥同步原理
參考:JUC鎖: LockSupport詳解 JUC鎖: ReentrantLock詳解 這兩個專題講解
3.1.3 synchronized和Lock對比
Lock應該確保在finally塊中釋放鎖,否則一旦同步代碼塊中拋出異常,則有可能永遠不會釋放持有的鎖。Lock必須由程式員來保證鎖釋放,而synchronized由Java虛擬機來確保即使出現異常,鎖也能被自動釋放。
3.2 非阻塞同步
非阻塞同步:樂觀併發策略:不管風險,先進行操作,如果沒有其他線程爭用共用數據,那操作就直接成功了。如果共用的數據的確被爭用,產生了衝突,那再進行其他的補償措施,最常用的補償措施是不斷地重試,直到出現沒有競爭的共用數據為止。樂觀併發策略的實現不再需要把線程阻塞掛起。
利用處理器指令,實現非阻塞同步
硬體保證某些從語義上看起來需要多次操作的行為可以只通過一條處理器指令就能完成,這類指令常用的有:
- 測試並設置(Test-and-Set)
- 獲取並增加(Fetch-and-Increment)
- 交換(Swap)
- 比較並交換(Compare-and-Swap :CAS)
- 載入鏈接/條件儲存(Load-Linked/Store-Conditional:LL/SC)
CAS的專題分析:JUC原子類: CAS, Unsafe和原子類詳解
3.3 無同步方案
如果方法不涉及共用數據,那自然不需要採用同步措施來保證其正確性。因此會有一些代碼天生就是線程安全的
3.3.1 可重入代碼
代碼定義:可以在代碼執行的任何時刻中斷它,轉而去執行另外一段代碼(包括遞歸調用它本身),而在控制權返回後,原來的程式不會出現任何錯誤,也不會對結果有所影響。
特征:
- 不依賴全局變數、堆數據、公用的系統資源
- 用到的狀態量都由參數中傳入
- 不調用非可重入的方法
3.3.2 線程本地存儲
代碼定義:共用數據保證只在同一個線程中執行
場景:消費隊列的架構模式(如“生產者-消費者”模式)都會將產品的消費過程限制在一個線程中消費完。
如果變數要被多線程訪問,使用volatile關鍵字將它聲明為“易變的”;如果變數線程獨享,通過java.lang.ThreadLocal類來實現線程本地存儲的功能。
ThreadLocal專題分析:Threadlocal源碼解讀