1. 程式人生 > 實用技巧 >併發程式設計004 --- Lock基本使用和原理

併發程式設計004 --- Lock基本使用和原理

前面提到,執行緒安全問題的源頭有如下三個:

1、快取帶來的可見性問題

2、執行緒切換帶來的原子性問題

3、編譯優化,指令重排,帶來的順序性問題

其中1和3可以通過java提供的volatile關鍵字解決,而問題2的解決就需要藉助java中的鎖

synchronized關鍵字

java提供了synchronized關鍵字來實現執行緒的同步,synchronized關鍵字可以用來修飾程式碼塊、普通方法和靜態方法;修飾程式碼塊時,必須指定鎖變數,修飾普通方法時,預設鎖變數為this,而修飾靜態方法,鎖變數預設為當前類的Class物件;並且synchronized是可重入的,即同一個執行緒能夠多次獲取到該鎖

ReentrantLock

synchronized鎖有如下的缺點:1、“很重” --- 值得商榷,因為JDK 1.6對其進行了優化;2、鎖被其他執行緒佔有時,只能等待,沒有額外的嘗試機制;因此引入了Lock介面類;

1、lock()方法類似於synchronize關鍵字,如果執行緒嘗試獲取鎖獲取不到,執行緒會一直處於等待狀態

2、lockInterruptibly()方法更為靈活,如果鎖獲取不到,那麼當前執行緒是可以響應中斷的

3、tryLock()方法,如果執行緒獲取不到鎖,那麼會直接返回false,還提供了一個過載方法:tryLock(time ms),在指定時間內獲取不到鎖,返回false

ReentrantLock實現了Lock介面,並且可以通過構造器的入參指定是否為公平策略:

公平策略指的是:多執行緒競爭鎖的時候,更傾向於將鎖的持有權交由等待時間最長的執行緒;

非公平策略指的是:多執行緒競爭鎖的時候,由OS決定鎖的持有權

需要注意的是:一般情況下,多執行緒公平策略下,吞吐量比較低,執行緒執行開銷大

ReentrantReadWriteLock

有這樣一個場景,一個應用程式中,讀取資料的次數遠遠大於寫輸入的次數,而讀資料的執行緒間一定不會有執行緒安全問題,如果使用前面的兩種鎖,會造成鎖的過度使用,因此引入了讀寫鎖ReentrantReadWriteLock

1、可重入讀寫鎖有兩個鎖,讀鎖和寫鎖,讀鎖為共享鎖,而寫鎖為獨佔鎖

2、檢測到有執行緒持有寫鎖,執行緒無法獲取到讀鎖,反之亦然,即讀寫互斥

3、鎖降級:執行緒A獲取到寫鎖後,修改共享變數,然後獲取讀鎖,接著釋放寫鎖,然後對共享變數做其他的操作,完成後釋放讀鎖;那麼鎖降級是否有必要呢?

答案是肯定的,在前面敘述的場景下,在寫執行緒釋放後,另一個執行緒B修改了共享變數,會引入執行緒安全問題,因此在釋放寫鎖前獲取讀鎖,這樣其他執行緒獲取寫鎖時會等待讀鎖的釋放

StampedLock

JDK1.8引入,前面的ReetrantReadWriteLock能夠解決讀多寫少場景效率問題,但是容易引起寫執行緒“飢餓”的問題,即:讀執行緒過多,寫執行緒一直無法獲得寫鎖,一直無法得到真正的執行

為了解決該問題,JDK1.8引入了StampedLock

1、StampedLock有三種應用場景:讀場景、寫場景、樂觀讀場景,樂觀讀場景即:認為讀期間,其他執行緒不會修改共享變數,這是一種樂觀的行為,但是在一些情況下會有執行緒安全問題,因此需要用額外的手段解決該問題;

官方提供了樂觀讀的例子,這裡整理為虛擬碼模板,為了避免使用中出現問題,在樂觀讀場景下要嚴格遵循該模板

long optStamped = lock.tryOptimisticRead();// 獲取樂觀讀鎖,實際只是獲取郵戳,並未真正獲取鎖
// 讀取共享變數操作。。。
if (!lock.validate(optStamped)) { // 其他執行緒做了寫操作
      optStamped  = lock.readLock(); // 獲取讀鎖
      try {
          // 重新讀取共享變數
      } finally {
          lock.unReadLock(optStamped);
      }
}

2、需要注意:StampedLock是不可重入的

3、支援讀寫鎖相互轉換

4、無論讀鎖還是寫鎖,都不支援condition