最近在忙公司的專案,現在終於有時間來寫部落格啦~開心開心
前言
通過前面的文章,我們已經瞭解了AQS(AbstractQueuedSynchronizer)
內部的實現與基本原理。現在我們來了解一下,Java中為我們提供的Lock機制下的鎖實現--ReentrantLock(重入鎖)
,閱讀該篇文章之前,希望你已閱讀以下文章。
- Java併發程式設計之鎖機制之Lock介面
- Java併發程式設計之鎖機制之AQS(AbstractQueuedSynchronizer)
- Java併發程式設計之鎖機制之LockSupport工具
- Java併發程式設計之鎖機制之Condition介面
ReentrantLock基本介紹
ReentrantLock
是一種可重入
的互斥鎖
,它具有與使用synchronized
方法和語句所訪問的隱式監視器鎖相同的一些基本行為和語義,但功能更強大。
ReentrantLock
將由最近成功獲得鎖,並且還沒有釋放該鎖的執行緒所擁有。當鎖沒有被另一個執行緒所擁有時,呼叫 lock 的執行緒將成功獲取該鎖並返回。如果當前執行緒已經擁有該鎖,此方法將立即返回。可以使用isHeldByCurrentThread()
和 getHoldCount()
方法來檢查此情況是否發生。
此類的構造方法接受一個可選的公平
引數。當設定為 true 時(也是當前ReentrantLock為公平鎖的情況
),在多個執行緒的爭用下,這些鎖傾向於將訪問權授予等待時間最長的執行緒。否則此鎖將無法保證任何特定訪問順序。與採用預設設定(使用不公平鎖)相比,使用公平鎖的程式在許多執行緒訪問時表現為很低的總體吞吐量(即速度很慢,常常極其慢),但是在獲得鎖和保證鎖分配的均衡性時差異較小。不過要注意的是,公平鎖不能保證執行緒排程的公平性。因此,使用公平鎖的眾多執行緒中的一員可能獲得多倍的成功機會,這種情況發生在其他活動執行緒沒有被處理並且目前並未持有鎖時。還要注意的是,未定時的 tryLock 方法並沒有使用公平設定。因為即使其他執行緒正在等待,只要該鎖是可用的,此方法就可以獲得成功。
ReentrantLock 類基本結構
通過上文的簡單介紹後,我相信很多小夥伴還是一臉懵逼,只知道上文我們提到了ReentrantLock
與synchronized
相比有相同的語義,同時其內部分為了公平鎖
與非公平鎖
兩種鎖的型別,且該鎖是支援重進入
的。那麼為了方便大家理解這些知識點,我們先從其類的基本結構講起。具體類結構如下圖所示:
從上圖中我們可以看出,在ReentrantLock
類中,定義了三個靜態內部類,Sync、FairSync(公平鎖)、NonfairSync(非公平鎖)。其中Sync
繼承了AQS(AbstractQueuedSynchronizer)
,而FairSync
與NonfairSync
又分別繼承了Sync
。關於ReentrantLock
基本類結構如下所示:
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
//預設無參建構函式,預設為非公平鎖
public ReentrantLock() {
sync = new NonfairSync();
}
//帶引數的建構函式,使用者自己來決定是公平鎖還是非公平鎖
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//抽象基類繼承AQS,公平鎖與非公平鎖繼承該類,並分別實現其lock()方法
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
//省略部分程式碼..
}
//非公平鎖實現
static final class NonfairSync extends Sync {...}
//公平鎖實現
static final class FairSync extends Sync {....}
//鎖實習,根據具體子類實現呼叫
public void lock() {
sync.lock();
}
//響應中斷的獲取鎖
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
//嘗試獲取鎖,預設採用非公平鎖方法實現
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
//超時獲取鎖
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
//釋放鎖
public void unlock() {
sync.release(1);
}
//建立鎖條件(從Condetion來理解,就是建立等待佇列)
public Condition newCondition() {
return sync.newCondition();
}
//省略部分程式碼....
}
複製程式碼
這裡為了方便大家理解
ReentrantLock
類的整體結構,我省略了一些程式碼及重新排列了一些程式碼的順序。
從程式碼中我們可以看出。整個ReentrantLock
類的實現其實都是交給了其內部FairSync
與NonfairSync
兩個類。在ReentrantLock
類中有兩個建構函式,其中不帶引數的建構函式中預設使用的NonfairSync(非公平鎖)
。另一個帶引數的建構函式,使用者自己來決定是FairSync(公平鎖)
還是非公平鎖。
重進入實現
在上文中,我們提到了ReentrantLock
是支援重進入的,那什麼是重進入呢?重進入是指任意執行緒在獲取到鎖之後能夠再次獲取該鎖,而不會被鎖阻塞
。那接下來我們看看這個例子,如下所示:
class ReentrantLockDemo {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
methodA();
}
});
thread.start();
}
public static void methodA() {
lock.lock();
try {
System.out.println("我已經進入methodA方法了");
methodB();//方法A中繼續呼叫方法B
} finally {
lock.unlock();
}
}
public static void methodB() {
lock.lock();
try {
System.out.println("我已經進入methodB方法了");
} finally {
lock.unlock();
}
}
}
//輸出結果
我已經進入methodA方法了
我已經進入methodB方法了
複製程式碼
在上述程式碼中我們宣告瞭一個執行緒呼叫methodA()方法。同時在該方法內部我們又呼叫了methodB()方法。從實際的程式碼執行結果來看,當前執行緒進入方法A之後。在方法B中再次呼叫lock.lock();
時,該執行緒並沒有被阻塞。也就是說ReentrantLock
是支援重進入的。那下面我們就一起來看看其內部的實現原理。
因為ReenTrantLock
將具體實現交給了NonfairSync(非公平鎖)
與FairSync(公平鎖)
。同時又因為上述提到的兩個鎖,關於重進入的實現又非常相似。所以這裡將採用NonfairSync(非公平鎖)
的重進入的實現,來進行分析。希望讀者朋友們閱讀到這裡的時候需要注意,不是我懶哦,是真的很相似哦。
好了下面我們來看程式碼。關於NonfairSync程式碼如下所示:
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))////直接獲取同步狀態成功,那麼就不再走嘗試獲取鎖的過程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
複製程式碼
當我們呼叫lock()方法時,通過CAS操作將AQS中的state的狀態設定為1,如果成功,那麼表示獲取同步狀態成功。那麼會接著呼叫setExclusiveOwnerThread(Thread thread)
方法來設定當前佔有鎖的執行緒。如果失敗,則呼叫acquire(int arg)
方法來獲取同步狀態(該方法是屬於AQS中的獨佔式獲取同步狀態的方法,對該方法不熟悉的小夥伴,建議閱讀Java併發程式設計之鎖機制之AQS(AbstractQueuedSynchronizer))。而該方法內部會呼叫tryAcquire(int acquires)
來嘗試獲取同步狀態。通過觀察,我們發現最終會呼叫Sync
類中的nonfairTryAcquire(int acquires)
方法。我們繼續跟蹤。
final boolean nonfairTryAcquire(int acquires) {
//獲取當前執行緒
final Thread current = Thread.currentThread();
int c = getState();
//(1)判斷同步狀態,如果未設定,則設定同步狀態
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//(2)如果當前執行緒已經獲取了同步狀態,則增加同步狀態的值。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
複製程式碼
從程式碼上來看,該方法主要走兩個步驟,具體如下所示:
- (1)先判斷同步狀態, 如果未曾設定,則設定同步狀態,並設定當前佔有鎖的執行緒。
- (2)判斷是否是同一執行緒,如果當前執行緒已經獲取了同步狀態(也就是獲取了鎖),那麼增加同步狀態的值。
也就是說,如果同一個鎖獲取了鎖N(N為正整數
)次,那麼對應的同步狀態(state)
也就等於N。那麼接下來的問題來了,如果當前執行緒重複N次獲取了鎖,那麼該執行緒是否需要釋放鎖N次呢?
答案當然是必須的。當我們呼叫ReenTrantLock
的unlock()方法來釋放同步狀態(也就是釋放鎖)時,內部會呼叫sync.release(1);
。最終會呼叫Sync
類的tryRelease(int releases)
方法。具體程式碼如下所示:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
複製程式碼
從程式碼中,我們可以知道,每呼叫一次unlock()
方法會將當前同步狀態減一。也就是說如果當前執行緒獲取了鎖N次,那麼獲取鎖的相應執行緒也需要呼叫unlock()
方法N次。這也是為什麼我們在之前的重入鎖例子中,為什麼methodB
方法中也要釋放鎖的原因。
非公平鎖
在ReentrantLock中有著非公平鎖
與公平鎖
的概念,這裡我先簡單的介紹一下公平
這兩個字的含義。這裡的公平是指執行緒獲取鎖的順序。也就是說鎖的獲取順序是按照當前執行緒請求的絕對時間順序,當然前提條件下是該執行緒獲取鎖成功。
那麼接下來,我們來分析在ReentrantLock中的非公平鎖的具體實現。
這裡需要大傢俱備
AQS(AbstractQueuedSynchronizer)
類的相關知識。如果大家不熟悉這塊的知識。建議大家閱讀Java併發程式設計之鎖機制之AQS(AbstractQueuedSynchronizer)。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))//直接獲取同步狀態成功,那麼就不再走嘗試獲取鎖的過程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//省略部分程式碼...
}
複製程式碼
當在ReentrantLock在非公平鎖的模式下
,去呼叫lock()方法。那麼接下來最終會走AQS(AbstractQueuedSynchronizer)
下的acquire(int arg)(獨佔式的獲取同步狀態)
,也就是如下程式碼:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
複製程式碼
那麼結合之前我們所講的AQS知識,在多個執行緒在獨佔式
請求共享狀態下(也就是請求鎖)的情況下,在AQS中的同步佇列中的執行緒節點情況如下圖所示:
那麼我們試想一種情況,當Nod1中的執行緒執行完相應任務後,釋放鎖後。這個時候本來該喚醒當前執行緒節點的下一個節點
,也就是Node2中的執行緒
。這個時候突然另一執行緒突然來獲取執行緒(這裡我們用節點Node5
來表示)。具體情況如下圖所示:
那麼根據AQS中獨佔式獲取同步狀態的邏輯。只要Node5對應的執行緒獲取同步狀態成功
。那麼就會出現下面的這種情況,具體情況如下圖所示:
從上圖中我們可以看出,由於Node5物件的執行緒搶佔了獲取同步狀態(獲取鎖)的機會,本身應該被喚醒的Node2
執行緒節點。因為獲取同步狀態失敗。所以只有再次的陷入阻塞。那麼綜上。我們可以知道。非公平鎖獲取同步狀態(獲取鎖)時不會考慮同步佇列中中等待的問題。會直接嘗試獲取鎖。也就是會存在後申請,但是會先獲得同步狀態(獲取鎖)的情況。
公平鎖
理解了非公平鎖,再來理解公平鎖就非常簡單了。下面我們來看一下公平鎖與非公平鎖的加鎖的原始碼:
從原始碼我們可以看出,非公平鎖與公平鎖之間的程式碼唯一區別就是多了一個判斷條件!hasQueuedPredecessors()(圖中紅框所示)
。那我們檢視其原始碼(該程式碼在AQS中,強烈建議閱讀Java併發程式設計之鎖機制之AQS(AbstractQueuedSynchronizer))
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
複製程式碼
程式碼理解理解起來非常簡單,就是判斷當前當前head節點的next節點是不是當前請求同步狀態(請求鎖)的執行緒。也就是語句
((s = h.next) == null || s.thread != Thread.currentThread()
。那麼接下來結合AQS中的同步佇列我們可以得到下圖:
那麼綜上我們可以得出,公平鎖保證了執行緒請求的同步狀態(請求鎖)的順序。不會出現另一個執行緒搶佔的情況。
最後
該文章參考以下圖書,站在巨人的肩膀上。可以看得更遠。
- 《Java併發程式設計的藝術》