深入瞭解jvm-2Edition-執行緒安全與鎖優化
1、什麼是執行緒安全
Brian Goetz 在 《Java Concurrency In Practice》中定義的執行緒安全為:
“當多執行緒訪問一個物件時,無論這些執行緒在執行時環境下以何種方式排程和交替執行,
在使用物件時,都不需要任何額外的同步,就可以得到正確的行為和結果,那麼這個物件就是執行緒安全的。”
Java中的執行緒安全可分為:
1、不可變
物件被安全構造(安全釋出)後狀態永不改變,那麼永遠是執行緒安全的。
2、絕對執行緒安全
滿足Brian Goetz 的執行緒安全的定義。
3、相對執行緒安全
只保證對物件單獨的操作是執行緒安全的,複合操作還是要進行同步。
4、執行緒相容
物件本身不是執行緒安全的,但是可以通過呼叫端使用同步來保證安全使用。
5、執行緒對立
無論是否採取同步,都無法安全併發使用的程式碼。
如:Thread.suspend() 和 Thread.resume() 有死鎖風險。
2、執行緒安全實現方法
1、互斥同步-阻塞同步
通過使用互斥來保證同步。
臨界區、互斥量、訊號量。
synchronized同步程式碼塊:
可重入、阻塞或喚醒需要作業系統完成。非公平,只能繫結一個喚醒條件。
ReentrantLock:
可重入,等待可中斷、可實現公平、可繫結多個喚醒條件。
因為jdk對synchronized做了優化,能用synchronized實現需求時儘量使用synchronized。
2、非阻塞同步
阻塞同步是一種悲觀的併發策略,認為只要沒有正確的同步措施,就一定會出現問題。
非阻塞同步則是樂觀的,基於衝突檢測,沒有衝突就算成功,有衝突就解決衝突(重新試)。
樂觀併發策略需要硬體幫助,因為要保證操作和檢測衝突是原子的。
常用的原子指令有:
Test-and-Set;
Fetch-and-Increment;
Swap;
Compare-and-Swap;
Load-Linked / Store-Conditional。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class ConcurrencyWithAtomic { private static final int THREADS_COUNT=20; public static AtomicInteger race=new AtomicInteger(0); public static void increase(){ race.incrementAndGet(); } public static void main(String[] args) throws InterruptedException { ExecutorService executorService= Executors.newCachedThreadPool(); for(int i=0; i<THREADS_COUNT; i++) { executorService.execute(()->{ for(int j=0; j<THREADS_COUNT; j++) increase(); }); } while(!executorService.awaitTermination(1000, TimeUnit.MILLISECONDS)) executorService.shutdown(); System.out.println(race); } }
其中incrementAndGet的原始碼為:
/** * Atomically increments the current value, * with memory effects as specified by {@link VarHandle#getAndAdd}. * * <p>Equivalent to {@code addAndGet(1)}. * * @return the updated value */ public final int incrementAndGet() { return U.getAndAddInt(this, VALUE, 1) + 1; } /** * Atomically adds the given value to the current value of a field * or array element within the given object {@code o} * at the given {@code offset}. * * @param o object/array to update the field/element in * @param offset field/element offset * @param delta the value to add * @return the previous value * @since 1.8 */ @IntrinsicCandidate public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!weakCompareAndSetInt(o, offset, v, v + delta)); return v; }
可以看到,getAndAddInt(Object o, long offset, int delta) 方法是實現了棧封閉的。
3、無同步方案
可重入程式碼,棧封閉;
ThreadLocal實現執行緒封閉。
3、鎖優化
1、旋轉鎖和自適應鎖
由於在掛起和恢復執行緒時,需要陷入核心,因此開銷較大。
因此,選擇在鎖持續時間短時,採用忙等待的形式。
自適應鎖就是虛擬機器會自動設定忙等待的迴圈次數。
2、鎖消除
由逃逸分析資料支援,在實現了棧封閉時,就不需要鎖了。
這時候就可以將鎖消除掉。
3、鎖粗化
我們寫程式碼時,為了減少同步操作的數量,總是習慣將同步程式碼塊縮小。
但是,如果同步程式碼塊小而密集的時候,頻繁的進行互斥操作會導致效能損耗。
這是,虛擬機器會將同步程式碼塊的範圍擴大。
4、輕量級鎖
在沒有多執行緒競爭的時候,減少互斥產生的效能消耗。
HotSpot虛擬機器物件頭結構:
物件頭分為兩部分,一部分用於存貯物件自身的執行時資料(HashCode、GC Age)。
另一部分用於儲存指向方法區中對應的元資料的引用。
第一部分也被稱為Mark Word,HotSpot虛擬機器Mark Word採用了非固定的資料結構。
在每種結構下,都有一個標誌位。
結構和標誌位一起確定了物件的狀態。
線上程進入同步程式碼塊時,如果此時物件沒有被鎖定,
虛擬機器將首先線上程的棧幀中建立一個鎖記錄空間(Lock Record),用於儲存物件的當前Mark Word的拷貝。
然後,虛擬機器使用CAS操作將物件Mark Word的值更新為指向鎖記錄空間的指標。
如果更新成功,那麼該物件就被輕量級鎖定了,Mark Word的標誌位變為00。
如果更新失敗,檢查物件的Mark Word是否指向當前執行緒,是,則可進入同步程式碼塊。
否,則說明物件已經被其他執行緒鎖定了。
此時,出現了兩個以上的執行緒爭用同一個鎖,輕量級鎖不再起效,要膨脹為重量級鎖。
鎖標記的標記位要變為 10。
釋放鎖的時候,也要通過CAS操作來完成。如果替換失敗,要釋放鎖的同時,喚醒被掛起的執行緒。
偏向鎖:
偏向鎖會偏向於第一個獲得它的執行緒,如果在接下來的執行過程中,該鎖沒有被其他執行緒獲取,
則持有鎖的執行緒不需要再進行同步。
一旦有執行緒嘗試獲取這個鎖,偏向模式就結束。