ReentrantLock——重入鎖

ReentrantLock的在AQS與簡介源碼分析中有所提及,本篇將用JDK1.8深入探索它內在的含義以及公平鎖和非公平鎖的性能測試比較

ReentrantLock實現了鎖接口,鎖接口提供比使用同步方法和語句有着更靈活的結構化,此外它還提供了其他功能,包括定時的鎖等待,可中斷的鎖等待並且可以支持多個關聯條件對象關於條件後續博文會單獨解釋)。


所有Lock的實現類都必須提供與內部鎖相同的內存語義,與synchronized相同的互斥性和內存可見性:

 如圖1所示,成功的鎖定操作與成功的鎖定操作具有相同的內存同步效果。

 2,成功的解鎖操作與成功的解鎖操作具有相同的內存同步效果。

 3,不成功的鎖定和解鎖操作以及重入鎖定/解鎖操作不需要任何內存同步效果


使用模版方式如下:

鎖定中提供瞭如下方法


的ReentrantLock只提供獨佔式獲取同步狀態的操作,提供了獲取鎖時的公平和非公平選擇,它的構造函數中提供了兩種公平性選擇,默認的構造函數是創建非公平鎖(參見下圖) 。在公平的鎖上,線程將按照它們發出請求的順序來獲得鎖,但在非公平的鎖上,則允許“插隊”:當一個線程持有非公平的鎖時,在執行相關邏輯後釋放了鎖,這個時候該鎖的狀態變爲可用,那麼這個線程可能跳過隊列中所有的等待線程並獲得該鎖,這也是公平鎖和非公平鎖實質的區別。

非公平鎖的獲取


1,Sync爲ReentrantLock裏面的一個內部類,它繼承AQS,有兩個子類:公平鎖FairSync和非公平鎖NonfairSync。

2,通過cas獲取同步狀態,將同步狀態設置爲1,這需要當前線程設置成爲獨佔式線程的原因是給鎖可以重入做鋪墊的用的。

3,如果沒有獲取到鎖,則調用AQS的獲取(1)方法

acquireQueued(addWaiter(Node.EXCLUSIVE),arg))這個方法如果有不清楚的請參考AQS簡介與源碼剖析這裏調用ReentrantLock的tryAcquire方法近而調用nonfairTryAcquire獲取同步狀態


這裏主要做了兩件事:

如圖1所示,若線程狀態= 0的狀態,嘗試獲取同步狀態成功則將當前線程設置成獨佔式線程。

2,若狀態!= 0,判斷當前線程是否爲獨佔式線程,如果是則說明鎖重入了,線程的同步狀態值的狀態增加


非公平鎖的釋放


調用AQS的release方法,如果釋放鎖成功則喚醒後繼節點嘗試獲取同步狀態,來着重看下tryRelease(int releases)

tryRelease的方法如下幾件事:

1,獲取當前線程的同步狀態值的狀態減去的版本中,賦值給℃。

2,如果當前線程不等於獨佔式線程則拋出llegalMonitorStateException異常

3,如果c等於0,說明沒有線程持有該鎖了,這個時候需要將獨佔式線程顯示的設置爲null,鎖釋放成功。

如圖4所示,如果!C = 0,則設置同步狀態值的狀態爲C,這個時候鎖沒有釋放成功。

這裏需要說明的是:

如果這個鎖被獲取了n次,那麼前(n-1)次tryRelease(int releases)方法必須返回false,而只有同步狀態完全釋放了,才能返回true。可以看到,該方法將同步狀態是否爲0作爲最終釋放的條件,當同步狀態爲0時,將佔有線程設置爲空(如果不將佔有線程設置爲空這個時候鎖還可以重入)並返回真,表示釋放成功。


公平鎖的獲取

公平鎖的獲取流程按下圖從上而下:


的tryAcquire的解釋可以參考AQS簡介與源碼分析與nonfairTryAcquire比較,唯一不同的位置爲判斷條件多了hasQueuedPredecessors()方法,即加入了同步隊列中當前節點是否有前驅節點的判斷,如果該方法返回true,則則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之後才能繼續獲取鎖


公平鎖的釋放

公平鎖和非公平鎖的釋放都是一套代碼一樣的邏輯


公平鎖與非公平鎖性能測試


public class FairAndUnFair {
private
static Lock fairLock = new ReentrantLock2(true);
private static Lock unfairLock = new ReentrantLock2(false);
私人 空隙 testLock)   {
 對於詮釋 I = 0 ; I <= 4 ;我++){
     線程線程= 線程(作業(鎖定)){
         公共字符串的toString {
             return getName();
         }
     };
     thread.setName( thread- + i);
     thread.start();
 }
}
私人 靜態 作業 擴展 線程 {
   私人鎖定 ;
   公共 職務鎖定鎖定 {
       this鎖定 = 鎖定 ;
   }
   public void run {
       forint i = 0 ; i <= 1 ; i ++){
           locklock();
           嘗試 {
               String currentThreadname = Thread.currentThread()。getName();
               Collection <Thread> queuedThreads =((ReentrantLock2)lock).getQueuedThreads();
               系統。out .println(“Lock by” + currentThreadname + “,等待” + queuedThreads);
           } 捕獲(例外五){
               e.printStackTrace();
           } 終於 {
               。開鎖();
           }
       }
   }
}
private static class ReentrantLock2 extends ReentrantLock {
   public ReentrantLock2boolean fair {
       super(fair); }
   }
   public Collection <Thread> getQueuedThreads {
       List <Thread> arrayList = new ArrayList <Thread>(super。getQueuedThreads
               ());
       Collections.reverse(ArrayList的);
       返回 arrayList;
   }
}
@ Test
public void fair)  
{
   testLock(fairLock);
}
@ 測試
public void void unfair)拋出BrokenBarrierException,InterruptedException
{
   testLock(unfairLock);
}
}

經過的JUnit測試結果如下

非公平鎖:

公平鎖:

對比上面的結果:

公平性鎖每次都是從同步隊列中的第一個節點獲取到鎖,而非公平性鎖出現了一個線程連續獲取鎖的情況。

原因是:非公平鎖只要線程獲取同步狀態就表示成功獲取到鎖,如果線程剛釋放了鎖,那麼這個時候會很可能再次獲取同步狀態,使得其他線程只能在同步隊列中等待。


下面運行測試用例

測試環境:

測試場景:

10個線程,每個線程獲取100000次鎖,通過vmstat的統計測試運行時系統線程上下文切換的次數

vmstat是Virtual Meomory Statistics(虛擬內存統計)的縮寫,可對操作系統的虛擬內存,進程,IO讀寫,CPU活動,每秒上下文切換等進行監視。

public class FairAndUnfairLockTest {
private static Lock lock = new ReentrantLock2(true);
私人
靜態 作業 實現 Runnable {
   私人鎖定 ;
   私人 CyclicBarrier cyclicBarrier;
   公共 作業鎖定,CyclicBarrier cyclicBarrier {
       this鎖定 = 鎖定 ;
       這個 .cyclicBarrier = cyclicBarrier;
   }
   public void run {
       forint i = 0 ; i < 100000 ; i ++){
           locklock();
           嘗試 {
               系統。out .println(i + “獲取鎖的當前線程[” + Thread.currentThread()。getName()+ “],同步隊列中的線程” +((ReentrantLock2)lock).getQueuedThreads());
           } finally {
               //一定要記得釋放鎖
               lock .unlock();
           }
       }
       try {
           // CyclicBarrier理解成柵欄直到十個線程都達到這裏然後打開柵欄放行
           cyclicBarrier。等待();
       } catch(InterruptedException e){
           e.printStackTrace();
       } catch(BrokenBarrierException e){
           e.printStackTrace();
       }
   }
}
/ **
*獲取同步隊列中的線程
* /

private static class ReentrantLock2 extends ReentrantLock {
   public ReentrantLock2boolean fair {
       super(fair); }
   }
   //列表是逆序輸出,爲了方便觀察結果,將其進行反轉
   public Collection <Thread> getQueuedThreads {
       List <Thread> arrayList = new ArrayList <Thread>(super。getQueuedThreads
               ());
       Collections.reverse(ArrayList的);
       返回 arrayList;
   }
}
/ **
*十個線程運行結束後計算執行時長
* /

private static class CountTime implements Runnable {
   //公平鎖,非公平鎖
   private String lockMode;
   //記錄的開始時間
   private long beginTime;
   public CountTimeString lockMode,long beginTime {
       this.beginTime = beginTime;
       這個 .lockMode = lockMode;
   }
   public void run {
       系統。out .println(lockMode + “執行時長:” + String.valueOf(System.currentTimeMillis() - beginTime));
   }
}
public static void mainString [] args {
   //公平鎖
   字符串lockMode = “公平鎖定” ; }
   long beginTime = System.currentTimeMillis();
   // 10個線程執行完成後,再執行CountTime線程統計執行時間
   CyclicBarrier cyclicBarrier = new CyclicBarrier(10new CountTime(lockMode,beginTime));
   forint i = 0 ; i <10 ; i ++){
       Thread thread = new Thread(new Job(lock,cyclicBarrier)){
           //如果不覆寫toString的話線程名看着不太清晰
           public String toString {
               return getName();
           }
       };
       thread.setName( thread- + i);
       thread.start();
   }
}
}

公平鎖執行結果:


非公平鎖執行結果:

CS表示:每秒上下文切換數

vmstat 1:每1秒採集數據一次

在測試中公平性鎖與非公平性鎖相比,總耗時是其2.1倍。可以看出,公平性鎖保證了鎖的獲取按照先進先出原則,而代價是進行大量的線程切換。非公平性雖鎖然可能造成線程“飢餓”,但極少的線程切換,保證了其更大的吞吐量。



參考:

Doug Lea:“Java併發編程實戰”

方騰飛,魏鵬,程曉明:“Java併發編程的藝術”

CSDN文章同步會慢些,歡迎關注微信公衆號:挨踢男孩


    



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章