重入鎖(ReentrantLock)是一種遞歸無阻塞的同步機制。以前一直認為它是synchronized的簡單替代,而且實現機制也不相差太遠。不過最近實踐過程中發現它們之間還是有著天壤之別。
以下是官方說明:一個可重入的互斥鎖定 Lock,它具有與使用 synchronized 方法和語句所訪問的隱式監視器鎖定相同的一些基本行為和語義,但功能更強大。ReentrantLock 將由最近成功獲得鎖定,并且還沒有釋放該鎖定的線程所擁有。當鎖定沒有被另一個線程所擁有時,調用 lock 的線程將成功獲取該鎖定并返回。如果當前線程已經擁有該鎖定,此方法將立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法來檢查此情況是否發生。
它提供了lock()方法:
如果該鎖定沒有被另一個線程保持,則獲取該鎖定并立即返回,將鎖定的保持計數設置為 1。
如果當前線程已經保持該鎖定,則將保持計數加 1,并且該方法立即返回。
如果該鎖定被另一個線程保持,則出于線程調度的目的,禁用當前線程,并且在獲得鎖定之前,該線程將一直處于休眠狀態,此時鎖定保持計數被設置為 1。
最近在研究Java concurrent中關于任務調度的實現時,讀了延遲隊列DelayQueue的一些代碼,比如take()。該方法的主要功能是從優先隊列(PriorityQueue)取出一個最應該執行的任務(最優值),如果該任務的預訂執行時間未到,則需要wait這段時間差。反之,如果時間到了,則返回該任務。而offer()方法是將一個任務添加到該隊列中。
后來產生了一個疑問:如果最應該執行的任務是一個小時后執行的,而此時需要提交一個10秒后執行的任務,會出現什么狀況?還是先看看take()的源代碼:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null) {
available.await();
} else {
long delay = first.getDelay(TimeUnit.NANOSECONDS);
if (delay > 0) {
long tl = available.awaitNanos(delay);
} else {
E x = q.poll();
assert x != null;
if (q.size() != 0)
available.signalAll(); // wake up other takers
return x;
}
}
}
} finally {
lock.unlock();
}
}
而以下是offer()的源代碼:
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
q.offer(e);
if (first == null || e.compareTo(first) < 0)
available.signalAll();
return true;
} finally {
lock.unlock();
}
}
如代碼所示,take()和offer()都是lock了重入鎖。如果按照synchronized的思維(使用諸如synchronized(obj)的方法),這兩個方法是互斥的。回到剛才的疑問,take()方法需要等待1個小時才能返回,而offer()需要馬上提交一個10秒后運行的任務,會不會一直等待take()返回后才能提交呢?答案是否定的,通過編寫驗證代碼也說明了這一點。這讓我對重入鎖有了更大的興趣,它確實是一個無阻塞的鎖。
下面的代碼也許能說明問題:運行了4個線程,每一次運行前打印lock的當前狀態。運行后都要等待5秒鐘。
public static void main(String[] args) throws InterruptedException {
final ExecutorService exec = Executors.newFixedThreadPool(4);
final ReentrantLock lock = new ReentrantLock();
final Condition con = lock.newCondition();
final int time = 5;
final Runnable add = new Runnable() {
public void run() {
System.out.println("Pre " + lock);
lock.lock();
try {
con.await(time, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("Post " + lock.toString());
lock.unlock();
}
}
};
for(int index = 0; index < 4; index++)
exec.submit(add);
exec.shutdown();
}
這是它的輸出:
Pre ReentrantLock@a59698[Unlocked]
Pre ReentrantLock@a59698[Unlocked]
Pre ReentrantLock@a59698[Unlocked]
Pre ReentrantLock@a59698[Unlocked]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-2]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-3]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-4]
每一個線程的鎖狀態都是“Unlocked”,所以都可以運行。但在把con.await改成Thread.sleep(5000)時,輸出就變成了:
Pre ReentrantLock@a59698[Unlocked]
Pre ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Pre ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Pre ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-1]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-2]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-3]
Post ReentrantLock@a59698[Locked by thread pool-1-thread-4]
以上的對比說明線程在等待時(con.await),已經不在擁有(keep)該鎖了,所以其他線程就可以獲得重入鎖了。
有必要會過頭再看看Java官方的解釋:“如果該鎖定被另一個線程保持,則出于線程調度的目的,禁用當前線程,并且在獲得鎖定之前,該線程將一直處于休眠狀態”。我對這里的“保持”的理解是指非wait狀態外的所有狀態,比如線程Sleep、for循環等一切有CPU參與的活動。一旦線程進入wait狀態后,它就不再keep這個鎖了,其他線程就可以獲得該鎖;當該線程被喚醒(觸發信號或者timeout)后,就接著執行,會重新“保持”鎖,當然前提依然是其他線程已經不再“保持”了該重入鎖。
總結一句話:對于重入鎖而言,"lock"和"keep"是兩個不同的概念。lock了鎖,不一定keep鎖,但keep了鎖一定已經lock了鎖。