ReentrantLock(重入鎖)以及公平性

簡介

ReentrantLock的實現不僅可以替代隱式的synchronized關鍵字,而且能夠提供超過關鍵字本身的多種功能。
這裏提到一個鎖獲取的公平性問題,如果在絕對時間上,先對鎖進行獲取的請求一定被先滿足,那麼這個鎖是公平的,反之,是不公平的,也就是說等待時間最長的線程最有機會獲取鎖,也可以說鎖的獲取是有序的。ReentrantLock這個鎖提供了一個構造函數,能夠控制這個鎖是否是公平的。
而鎖的名字也是說明了這個鎖具備了重複進入的可能,也就是說能夠讓當前線程多次的進行對鎖的獲取操作,這樣的最大次數限制是Integer.MAX_VALUE,約21億次左右。
事實上公平的鎖機制往往沒有非公平的效率高,因爲公平的獲取鎖沒有考慮到操作系統對線程的調度因素,這樣造成JVM對於等待中的線程調度次序和操作系統對線程的調度之間的不匹配。對於鎖的快速且重複的獲取過程中,連續獲取的概率是非常高的,而公平鎖會壓制這種情況,雖然公平性得以保障,但是響應比卻下降了,但是並不是任何場景都是以TPS作爲唯一指標的,因爲公平鎖能夠減少“飢餓”發生的概率,等待越久的請求越是能夠得到優先滿足。

實現分析

在ReentrantLock中,對於公平和非公平的定義是通過對同步器AbstractQueuedSynchronizer的擴展加以實現的,也就是在tryAcquire的實現上做了語義的控制。

非公平的獲取語義:

01 final boolean nonfairTryAcquire(int acquires) {
02     final Thread current = Thread.currentThread();
03     int c = getState();
04     if (c == 0) {
05         if (compareAndSetState(0, acquires)) {
06             setExclusiveOwnerThread(current);
07             return true;
08         }
09     else if (current == getExclusiveOwnerThread()) {
10         int nextc = c + acquires;
11                 if (nextc < 0// overflow
12             throw new Error("Maximum lock count exceeded");
13         setState(nextc);
14         return true;
15     }
16     return false;
17 }

上述邏輯主要包括:

  • 如果當前狀態爲初始狀態,那麼嘗試設置狀態;
  • 如果狀態設置成功後就返回;
  • 如果狀態被設置,且獲取鎖的線程又是當前線程的時候,進行狀態的自增;
  • 如果未設置成功狀態且當前線程不是獲取鎖的線程,那麼返回失敗。

公平的獲取語義:

01 protected final boolean tryAcquire(int acquires) {
02     final Thread current = Thread.currentThread();
03     int c = getState();
04     if (c == 0) {
05         if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
06             setExclusiveOwnerThread(current);
07             return true;
08         }
09     else if (current == getExclusiveOwnerThread()) {
10         int nextc = c + acquires;
11         if (nextc < 0)
12             throw new Error("Maximum lock count exceeded");
13         setState(nextc);
14         return true;
15     }
16     return false;
17 }

上述邏輯相比較非公平的獲取,僅加入了當前線程(Node)之前是否有前置節點在等待的判斷。hasQueuedPredecessors()方法命名有些歧義,其實應該是currentThreadHasQueuedPredecessors()更爲妥帖一些,也就是說當前面沒有人排在該節點(Node)前面時候隊且能夠設置成功狀態,才能夠獲取鎖。

釋放語義:

01 protected final boolean tryRelease(int releases) {
02     int c = getState() - releases;
03     if (Thread.currentThread() != getExclusiveOwnerThread())
04         throw new IllegalMonitorStateException();
05     boolean free = false;
06     if (c == 0) {
07         free = true;
08         setExclusiveOwnerThread(null);
09     }
10     setState(c);
11     return free;
12 }

上述邏輯主要主要計算了釋放狀態後的值,如果爲0則完全釋放,返回true,反之僅是設置狀態,返回false。
下面將主要的筆墨放在公平性和非公平性上,首先看一下二者測試的對比:
測試用例如下:

01 public class ReentrantLockTest {
02     private static Lock fairLock = new ReentrantLock(true);
03     private static Lock unfairLock = new ReentrantLock();
04  
05     @Test
06     public void fair() {
07         System.out.println("fair version");
08         for (int i = 0; i < 5; i++) {
09             Thread thread = new Thread(new Job(fairLock));
10             thread.setName("" + i);
11             thread.start();
12         }
13  
14         try {
15             Thread.sleep(5000);
16         catch (InterruptedException e) {
17             e.printStackTrace();
18         }
19     }
20  
21     @Test
22     public void unfair() {
23         System.out.println("unfair version");
24         for (int i = 0; i < 5; i++) {
25             Thread thread = new Thread(new Job(unfairLock));
26             thread.setName("" + i);
27             thread.start();
28         }
29  
30         try {
31             Thread.sleep(5000);
32         catch (InterruptedException e) {
33             e.printStackTrace();
34         }
35     }
36  
37     private static class Job implements Runnable {
38         private Lock lock;
39         public Job(Lock lock) {
40             this.lock = lock;
41         }
42  
43         @Override
44         public void run() {
45             for (int i = 0; i < 5; i++) {
46                 lock.lock();
47                 try {
48                     System.out.println("Lock by:"
49                             + Thread.currentThread().getName());
50                 finally {
51                     lock.unlock();
52                 }
53             }
54         }
55     }
56 }

調用非公平的測試方法,返回結果(部分):
unfair version
Lock by:0
Lock by:0
Lock by:2
Lock by:2
Lock by:2
Lock by:2
Lock by:2
Lock by:0
Lock by:0
Lock by:0
Lock by:1
Lock by:1
Lock by:1
調用公平的測試方法,返回結果:
fair version
Lock by:0
Lock by:1
Lock by:0
Lock by:2
Lock by:3
Lock by:4
Lock by:1
Lock by:0
Lock by:2
Lock by:3
Lock by:4
仔細觀察返回的結果(其中每個數字代表一個線程),非公平的結果一個線程連續獲取鎖的情況非常多,而公平的結果連續獲取的情況基本沒有。那麼在一個線程獲取了鎖的那一刻,究竟鎖的公平性會導致鎖有什麼樣的處理邏輯呢?
通過之前的同步器(AbstractQueuedSynchronizer)的介紹,在鎖上是存在一個等待隊列,sync隊列,我們通過複寫ReentrantLock的獲取當前鎖的sync隊列,輸出在ReentrantLock被獲取時刻,當前的sync隊列的狀態。
修改測試如下:

01 public class ReentrantLockTest {
02     private static Lock fairLock = new ReentrantLock2(true);
03     private static Lock unfairLock = new ReentrantLock2();
04     @Test
05     public void fair() {
06         System.out.println("fair version");
07         for (int i = 0; i < 5; i++) {
08             Thread thread = new Thread(new Job(fairLock)) {
09                 public String toString() {
10                     return getName();
11                 }
12             };
13             thread.setName("" + i);
14             thread.start();
15         }
16         // sleep 5000ms
17     }
18  
19     @Test
20     public void unfair() {
21         System.out.println("unfair version");
22         for (int i = 0; i < 5; i++) {
23             Thread thread = new Thread(new Job(unfairLock)) {
24                 public String toString() {
25                     return getName();
26                 }
27             };
28             thread.setName("" + i);
29             thread.start();
30         }
31         // sleep 5000ms
32     }
33  
34     private static class Job implements Runnable {
35         private Lock lock;
36  
37         public Job(Lock lock) {
38             this.lock = lock;
39         }
40  
41         @Override
42         public void run() {
43             for (int i = 0; i < 5; i++) {
44                 lock.lock();
45                 try {
46                     System.out.println("Lock by:"
47                             + Thread.currentThread().getName() + " and "
48                             + ((ReentrantLock2) lock).getQueuedThreads()
49                             " waits.");
50                 finally {
51                     lock.unlock();
52                 }
53             }
54         }
55     }
56  
57     private static class ReentrantLock2 extends ReentrantLock {
58         // Constructor Override
59  
60         private static final long serialVersionUID = 1773716895097002072L;
61  
62         public Collection<Thread> getQueuedThreads() {
63             return super.getQueuedThreads();
64         }
65     }
66 }

上述邏輯主要是通過構造ReentrantLock2用來輸出在sync隊列中的線程內容,而且每個線程的toString方法被重寫,這樣當一個線程獲取到鎖時,sync隊列裏的內容也就可以得知了,運行結果如下:
調用非公平方法,返回結果:
unfair version
Lock by:0 and [] waits.
Lock by:0 and [] waits.
Lock by:3 and [2, 1] waits.
Lock by:3 and [4, 2, 1] waits.
Lock by:3 and [4, 2, 1] waits.
Lock by:3 and [0, 4, 2, 1] waits.
Lock by:3 and [0, 4, 2, 1] waits.
Lock by:1 and [0, 4, 2] waits.
Lock by:1 and [0, 4, 2] waits.
調用公平方法,返回結果:
fair version
Lock by:0 and [] waits.
Lock by:1 and [0, 4, 3, 2] waits.
Lock by:2 and [1, 0, 4, 3] waits.
Lock by:3 and [2, 1, 0, 4] waits.
Lock by:4 and [3, 2, 1, 0] waits.
Lock by:0 and [4, 3, 2, 1] waits.
Lock by:1 and [0, 4, 3, 2] waits.
Lock by:2 and [1, 0, 4, 3] waits.
可以明顯看出,在非公平獲取的過程中,“插隊”現象非常嚴重,後續獲取鎖的線程根本不顧及sync隊列中等待的線程,而是能獲取就獲取。反觀公平獲取的過程,鎖的獲取就類似線性化的,每次都由sync隊列中等待最長的線程(鏈表的第一個,sync隊列是由尾部結點添加,當前輸出的sync隊列是逆序輸出)獲取鎖。一個 hasQueuedPredecessors方法能夠獲得公平性的特性,這點實際上是由AbstractQueuedSynchronizer來完成的,看一下acquire方法:

1 public final void acquire(int arg) {
2     if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
3         selfInterrupt();
4 }

可以看到,如果獲取狀態和在sync隊列中排隊是短路的判斷,也就是說如果tryAcquire成功,那麼是不會進入sync隊列的,可以通過下圖來深刻的認識公平性和AbstractQueuedSynchronizer的獲取過程。
非公平的,或者說默認的獲取方式如下圖所示:

對於狀態的獲取,可以快速的通過tryAcquire的成功,也就是黃色的Fast路線,也可以由於tryAcquire的失敗,構造節點,進入sync隊列中排序後再次獲取。因此可以理解爲Fast就是一個快速通道,當例子中的線程釋放鎖之後,快速的通過Fast通道再次獲取鎖,就算當前sync隊列中有排隊等待的線程也會被忽略。這種模式,可以保證進入和退出鎖的吞吐量,但是sync隊列中過早排隊的線程會一直處於阻塞狀態,造成“飢餓”場景。
而公平性鎖,就是在tryAcquire的調用中顧及當前sync隊列中的等待節點(廢棄了Fast通道),也就是任意請求都需要按照sync隊列中既有的順序進行,先到先得。這樣很好的確保了公平性,但是可以從結果中看到,吞吐量就沒有非公平的鎖高了。

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