1. 程式人生 > 實用技巧 >ReentrantLock-公平鎖、非公平鎖、互斥鎖、自旋鎖

ReentrantLock-公平鎖、非公平鎖、互斥鎖、自旋鎖

  重入鎖,又稱遞迴鎖,是指在同一執行緒中,外部方法獲取鎖後,內層遞迴方法仍然可以獲取該鎖。如果鎖不具備重入性,那麼當一個執行緒兩次獲取鎖的時候就會發生死鎖。java提供了java.util.concurrent.ReentrantLock來解決重入鎖問題。

  ReentrantLock重入鎖並不是容器集合類的一部分,但它在Concurrency包中佔據了非常重要的一部分。在併發容器的實現中被大量使用。

  ReentrantLock是一種顯式鎖,與synchronized隱式鎖對應。synchronized不能顯式的對Lock物件進行操作,因此有很多不便利性。而顯式鎖提供了多種方法來操作Lock。

  1) lock():獲取鎖,如果鎖不可用,那麼當前執行緒會休眠直到獲取鎖為止。

  2) lockInterruptibly():可中斷地獲取鎖,如果當前執行緒發生interrupt,則釋放鎖。

  3) tryLock():嘗試獲取鎖,如果取到了,那麼返回true,它與lock()的區別在於它不會休眠當前執行緒。

  4) unlock():釋放鎖。

  5) newCondition():建立一個當前鎖的條件監視器Condition,condition例項用於控制當前Lock的執行緒佇列的notify和wait。

  ReentrantLock的實現基於AQS,通過tryAcquire和tryRelease的重寫,實現了鎖機制和重入機制。

1,ReentrantLock的公平鎖與非公平鎖

  ReentrantLock在底層有兩種實現方式,分別是FairSync(公平鎖)和NonfairSync(非公平鎖),它們lock()流程如圖:

FairSync

1 static final class FairSync extends Sync {
2     private static final long serialVersionUID = -300003432432432L;
3     final void lock() {
4         //FairSync直接呼叫acquire方法來獲取鎖
5         acquire(1);
6 } 7 protected final boolean tryAcqure(int acquires) {...} 8 }

  FairSync與NonfairSync都會呼叫同樣的acquire方法,因此有必要了解一下acquire方法的實現:

1 public final void acquire(int arg) {
2     //只需要注意tryAcquire()方法,它用於請求鎖,返回true時後續的操作不再被處理
3     if(!tryAcquire(arg) && acquireQueued(addWaiter(Node, EXCLUSIVE), arg)) {
4         selfInterrupt();
5     }
6 }

tryAcquire用於請求鎖,當請求失敗的時候,會把當前執行緒加入等待佇列,addWaiter()和acquiredQueued()方法分別對應封裝等待執行緒節點和請求入隊操作。

NonfairSync

 1 static final class NonfairSync extends Sync {
 2     private static final long serialVesionUID = 4324242432L;
 3     final void lock() {
 4         //驗證當前鎖狀態,如果是0,那麼設定1
 5         //狀態為0說明沒有其他執行緒持有鎖,當前執行緒可以直接獲得鎖
 6         //setExclusiveOwnerThread即為了當前排它鎖執行所有者執行緒方法
 7         if(compareAndSetState(0, 1)) {
 8             setExclusiveOwnerThread(Thread.currentThread());
 9         } else {
10             //狀態不為0,則說明其他執行緒持有鎖,執行獲取鎖的方法acquire
11             //該方法最終用於獲取鎖的方法是tryAcquire
12             acquire();
13         }
14         
15         protected fianl boolean tryAcquire(int acquires) {
16             return nonfairTryAcquire(acquires);
17         }
18     }
19 }


  1) NonFairSync類在lock()方法呼叫的第一時間,直接驗證當前鎖狀態,如果沒有其它執行緒持有鎖(鎖狀態state為0),那麼當前執行緒會持有鎖。NonfairSync與fairSync主要區別為:

  2) NonFairSync類的tryAcquire()方法執行不同,它直接呼叫了nonfairTryAcquire()方法,nonfairTryAcquire()方法不要求嚴格按照等待佇列的入隊順序獲取鎖。

下面來看一下FairSync.tryAcquire()和NonFairSync.nonfairTryAcquire():

 1 protected final boolean tryAcquire(int acquires) {
 2     final Thread current = Thread.currentThread();
 3     int c = getState();
 4     if(c == 0) {
 5         //注意這個hasQueuedPredecessors()方法,只有FairSync才會呼叫它
 6         //它是FairSync和NonFairSync僅有的區別
 7         if(!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
 8             setExclusiveOwnerThread(current);
 9             return true;
10         } else if(current == getExclusiveOwnerThread()) {...}
11         return false;
12     }
13 }

  1) FairSync保證了FIFO,先入隊的等待執行緒會先獲得鎖,而NonfairSync任由各個等待執行緒競爭。

  2) 由於FairSync要保證有序性,所以NonfairSync的效能更高,ReentrantLock預設使用NonfairSync。1) FairSync保證了FIFO,先入隊的等待執行緒會先獲得鎖,而NonfairSync任由各個等待執行緒競爭。

2,ReentrantLock的重入性

加鎖有兩種基本形式,互斥鎖與自旋鎖。

互斥鎖(Mutex),通過阻塞執行緒來進行加鎖,中斷阻塞來進行解鎖。

 1 public class MutexLock {
 2     private AtomicReference<Thread> owner = new AtomicReference<>();
 3     private LinkedList<Thread> list = new LinkedList<>();
 4     public void lock() {
 5         Thread currentThread = Thread.currentThread();
 6         //沒有任何執行緒持有鎖時,讓當前執行緒持有鎖,反之則加入等待佇列並阻塞
 7         if(!owner.compareAndSet(null, currentThread)) {
 8             waiterQueue.add(currentThread);
 9             //LockSupport阻塞當前執行緒
10             LockSupport.park();
11         }
12     }
13     public void unlock() {
14         //如果解鎖的執行緒不是持有鎖的執行緒,那麼丟擲異常
15         if(Thread.currentThread() != owner.get()) {
16             throw new RuntimeException();
17         }
18         //等待佇列裡有內容時,恢復隊頭執行緒,更改持有鎖的執行緒,反之則直接釋放鎖
19         if(waiterQueue.size() > 0) {
20             Thread t = waiterQueue.poll();
21             owner.set(t);
22             //LockSupport釋放指定執行緒
23             LockSupport.unpark(t);
24         } else {
25             owner.set(null);
26         }
27     }
28 }

  自旋鎖(Spin lock),執行緒保持執行態,用一個迴圈體不停地判斷某個標質量的狀態來確定加鎖還是解鎖,本質上用一段無意義的死迴圈來阻塞執行緒執行。

 1 public class SpinLock {
 2     private AtomicReference<Thread> owner = new AtomicReference<>();
 3     public void lock() {
 4         Thread current = Thread.currentThread();
 5         //沒有任何執行緒持有鎖時,讓當前執行緒持有鎖,反之則利用迴圈來阻塞
 6         while (!owner.compareAndSet(null, current)) { }
 7     }
 8     public void unlock() {
 9         Thread current = Thread.currentThread();
10         //釋放鎖
11         owner.compareAndSet(current, null);
12     }
13 }

無論哪種實現方式,都回避不了一個問題,那就是在同一個執行緒中,如果遞迴地獲取相同的鎖,都會出現死鎖。設想執行緒A持有了鎖,在釋放之前,A再次請求加鎖,此時由於鎖擁有了持有者,於是由於鎖擁有了持有者(A自己),於是A被阻塞了。因此需要引入重入鎖: 自旋鎖(Spin lock),執行緒保持執行態,用一個迴圈體不停地判斷某個標質量的狀態來確定加鎖還是解鎖,本質上用一段無意義的死迴圈來阻塞執行緒執行。

  1) 線上程持有鎖的時候,其它執行緒不能訪問上鎖的共享資源。

  2) 線上程持有鎖的時候,執行緒本身可以繼續訪問上鎖的共享資源。

  3) 在多次遞迴訪問中,只有當全部訪問都結束了,執行緒才會釋放鎖。

  由此可以想到一個很直觀的解決方式——計數器,對持有鎖的執行緒的每一次訪問進行計數,只有當訪問次數清空之後,其他執行緒才能繼續訪問。

 1 public class MutexLock {
 2     private AtomicReference<Thread> owner = new AtomicReference<>();
 3     private LinkedList<Thread> waiterQueue = new LinkedList<>();
 4     private volatile AtomicInteger state = new AtomicInteger(0);
 5     public void lock() {
 6         Thread currentThread = Thread.currentThread();
 7         //如果請求鎖的執行緒是當前執行緒
 8         if(owner.get() == currentThread) {
 9             state.incrementAndGet();
10             return;
11         }
12         //沒有任何執行緒持有鎖時,讓當前執行緒持有鎖,反之則加入等待佇列並阻塞
13         if(!owner.compareAndSet(null, currentThread)) {
14             waiterQueue.add(currentThread);
15             //LockSupport阻塞當前執行緒
16             LockSupport.park();
17         }
18     }
19     public void unlock() {
20         //如果解鎖的執行緒不是持有鎖的執行緒,那麼丟擲異常
21         if(Thread.currentThread() != owner.get()) {
22             throw new RuntimeException();
23         }
24         //計數器清空之後才能繼續之後的操作
25         if(state.get() > 0) {
26             state.decrementAndGet();
27             return;
28         }
29         //等待佇列裡有內容時,釋放指定佇列,更改持有鎖的執行緒,反之則清空持有鎖的執行緒
30         if(waiterQueue.size() > 0) {
31             Thread t = waiterQueue.poll();
32             owner.set(t);
33             //LockSupport釋放指定執行緒
34             LockSupport.unpark(t);
35         } else {
36             owner.set(null);
37         }
38     }
39 }

  FairSync.tryAcquire()和NonfairSync.nofairTryAcquire()有重用部分,無論公平鎖還是非公平鎖,在處理重入上,程式碼是一致的:

  1) 判斷state標量是否為0,如果為0,那麼說明沒有執行緒持有該鎖,當前執行緒可以持有鎖,返回true;FairSync.tryAcquire()和NonfairSync.nofairTryAcquire()有重用部分,無論公平鎖還是非公平鎖,在處理重入上,程式碼是一致的:

  2) 如果state不為0,那麼判斷當前執行緒是否為鎖持有者。

  3) 如果不是,那麼當前執行緒不能持有鎖,返回false;

  4) 如果是,那麼當前執行緒已經持有鎖,此時認同執行緒請求次數增加,state需要增加acquires次,acquires表示新增的請求鎖次數。

 1 final boolean nonfairTryAcquire(int acquires) {
 2     final Thread current = Thread.currentThread();
 3     int c = getState();
 4     if(c == 0) {
 5         //...
 6     } else if(current == getExclusiveOwnerThread()) {
 7         int nextc = c + acquires;
 8         if(nextc < 0) {
 9             throw new Error("Maximum lock count exceeded");
10         }
11         setState(nextc);
12         return true;
13     }
14 }

tryRelease()方法有一個整型引數releases形參,用來表示本次釋放鎖的次數,如果當前執行緒不是鎖持有者,那麼說明這是一次非法呼叫,當state計數歸零的時候,呼叫setExclusiveOwnerThread(null),用來表示沒有執行緒持有鎖了,此後鎖可以被任意呼叫。

 1 protected final boolean tryRelease(int releases) {
 2     int c = getState() - releases;
 3     if(Thread.currentThread() != getExclusiveOwnerThread()) {
 4         throw new IllegalMonitorStateException;
 5     }
 6     boolean free = false;
 7     if(c == 0) {
 8         free = true;
 9         setExclusiveOwnerThread(null);
10     }
11     setState(c);
12     return free;
13 }