Java Concurrency #5: JUC —基本鎖類型 - Lock & Condition & ReentrantLock
前言
系列文到此開始會著重在Java Util Concurrent (JUC)套件的介紹,JUC套件在Java 1.5推出,比單純的使用Synchronized有更多的使用變化性,如同在死鎖章節提到的,要破除禁止搶佔(no preemption)死鎖子問題時,需要遇到lock時自動釋放目前佔有的資源(已被自己Lock住的資源),但Synchronized沒有提供釋放的方法給開發者釋放目前已占據的資源,而這時就需要JUC套件的幫忙了。
Lock Interface
如前言所說,JUC提供了比Synchronized更多的靈活性,而提供靈活性的項目包含
- 響應中斷 — 可以提供像是wait / notify的機制
- 支持超時 — 當Block住的時候可以,可以設定秒數釋放資源
- 不阻塞獲得鎖 — 在拿取鎖時不用像Synchronized需要再區塊外等待,可以判斷使否可以拿取,不可拿取可以先做其他事情
JUC藉由Lock Interface(java.util.concurrent.Lock)的方法,提供上面三個增加鎖應用靈活度的特性,在JUC包中每個有關Lock的物件都會實作此介面,提供以下五個基礎方法
public interface Lock {
//鎖
void lock();
//中斷
void lockInterruptibly() throws InterruptedException;
//非阻塞
boolean tryLock();
//時間內自動放棄嘗試獲取鎖
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//解鎖
void unlock();
//針對此Lock Object分出不同情境
Condition newCondition();
}
接下來以最常使用的ReentrantLock作為範例,講解Lock Interface每個方法的功能。
ReentranceLock
ReentranceLock根據字義就是可以重複進入的鎖,當解鎖後初始化的Lock還可以重複使用,基本的使用場景如下:
Basic use
鎖住關鍵資源的方式跟Synchronized不大一樣,Synchronized是隱式鎖JVM會在背後自動幫你加鎖與解鎖,如今直接使用JUC的Lock則需要自己解鎖,所以才需要再不會吐Exception的程式碼區塊做try and finnally。
Constructor (Fair & unFair)
ReentranceLock還有一個獨特的點,就是在建構者中給予是否公平的布林值,如果true就代表每個要拿取此Lock的執行緒都會盡可能的平均分配,如果false的話則是隨機應變了,如果沒有別需要則不需特別使用公平機制,因為會增加Lock底層的判斷降低效能。原始碼如下:
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
} /**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
Try Lock
接下來介紹Lock中的tryLock() / tryLock(int ms),有了基本Lock為何還需要tryLock?如果鎖被拿走,而其他Thread要Lock時就只能呆呆的等lock被釋放,而如果使用了tryLock,則會馬上告訴開發者拿取的結果,開發者還可以針對沒有拿到鎖做額外的處裡(紀錄或先暫時處理其他資料再回來)。
//Example 1
public void mockFileWriting(String input) {
if (lock.tryLock()) {
try {
mockFile += input;
} finally {
lock.unlock();
}
} else {
System.out.println("The file is already been using");
}
}
//Example 2
public void mockFileWriting(String input) {
if (lock.tryLock(2000)) {
try {
mockFile += input;
} finally {
lock.unlock();
}
} else {
System.out.println("The file is already been using");
}
Example 1的tryLock,只要在目前有人使用lock就會馬上回傳false,並且將邏輯導到else中。
Example 2的tryLock(2000),則會在嘗試拿取lock兩千毫秒都失敗的情況下才會導到else。
增加Lock靈活度最關鍵的Condition
回想系列文前面介紹的wait / notifyAll,如果只呼叫notify的話JVM會隨機喚醒一個正在wait的Thread,而notifyAll則會喚醒全部的Thread(若是Thread邏輯還是拿不到鎖的話則會繼續wait,有拿到鎖的才會往後進行)。但其實根據程式邏輯,只需要喚醒等待目前Lock被拿走的Thread就好,喚醒全部有點太多了而隨機喚醒如果沒喚醒到又會產生Bug,而Synchronized做不到目標喚醒但JUC可以,關鍵就在於Condition,我們擷取一小段Java LinkedBlockingDeque中的原始碼
/** Main lock guarding all access */
final ReentrantLock lock = new ReentrantLock(); /** Condition for waiting takes */
private final Condition notEmpty = lock.newCondition();
private boolean linkFirst(Node<E> node) {
// assert lock.isHeldByCurrentThread();
if (count >= capacity)
return false;
Node<E> f = first;
node.next = f;
first = node;
if (last == null)
last = node;
else
f.prev = node;
++count;
//喚醒 notEmpty await的執行緒操作
notEmpty.signal();
return true;
}
public E takeFirst() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E x;
while ( (x = unlinkFirst()) == null)
//沒有元素則先await等待Deque有元素
notEmpty.await();
return x;
} finally {
lock.unlock();
}
}
Condition就是lock實踐我們在Synchronized系列講解的wait/notify,而lock切分出不同的Condition就可以讓開發者針對不同的condition去做wait and notify,其中condition的await()就是wait(),而signal()就是notify不過是會針對目標去做notify。
上面的程式碼如果呼叫takeFirst,但是Deque沒有元素的化會進入wait狀態,直到被呼叫linkFirst之後才會被signal喚醒取出資料。
總結
本篇文章介紹了JUC最重要的Lock Interface,並且由最常使用的ReentrantLock做為範例實踐,和Synchronized最大的差別在於Lock提供了本來由JVM做掉的功能方法(lock / unlock),還提供了Condition讓開發者可以做更細節的wait/notify增加Concurrcny的效率。
最後的重點,JUC Lock提供了靈活的API介面也代表Java讓開發者自己管理Thread Concurrency,如果沒有足夠的信心與熟練,則會容易寫出造成死結Deadlock的程式碼,小心使用。