java併發程式設計之顯示鎖
顯示鎖
為了保證共享物件的安全性,常用的機制有:
- volatile 關鍵字
- synchronized
- ReentrantLock 顯示鎖
1.1 ReentrantLock
ReentrantLock實現了Lock介面。Lock介面定義一組抽象的加鎖操作。
Lock提供了一種無條件的、可輪詢的、定時的、以及可中斷的鎖獲取操作。
所有的加鎖、解鎖操作都是顯示的。在Lock的實現中必須提供與內部鎖相同的記憶體可見語義性,但是在加鎖語義、排程演算法、順序保證以及效能特性方面可以不同。
ReentrantLock和synchronized的共同點:都支援可重入鎖。
1.2 為什麼需要ReentrantLock鎖?
- 無法中斷一個正在等待獲取鎖的執行緒、
- 無法再請求獲取一個鎖時無限地等待下去。
- 內建鎖必須在獲取該鎖的程式碼塊中釋放,無法實現非租塞結構的加鎖規則。
ReentrantLock的標準用法:
public static void main(String[] args) {
Lock lock = new ReentrantLock();
lock.lock();
try {
// 這裡是你的邏輯... 捕獲並處理異常
} finally {
// 如果沒有使用finally釋放鎖,會相當危險,不像synchronize,程式離開控制塊時候,會自動釋放鎖。
lock.unlock();
}
}
1.3 可定時鎖和輪詢鎖
在內建鎖中,防止死鎖的唯一方式是在構建程式時避免出現不一樣的鎖順序。
但是,可定時和可輪詢的鎖提供了另一種防止死鎖的機制:
如果不能獲得全部的鎖,那麼可以使用可以定時的或者是可輪詢的鎖獲取方式。它會釋放已經獲得的鎖,然後重新嘗試獲取其它鎖。
定時鎖:在帶有時間限制的操作中呼叫一個阻塞方法,它能根據剩餘時間提供一個時限,如果操作不能在指定時間內給出結果,那麼程式會提前結束。然後使用內建鎖時,開始請求鎖操作後,這個操作無法取消。
定時鎖的API:
//嘗試非阻塞的獲取鎖,呼叫該方法後立刻返回,如果獲得鎖返回true,否則返回false
boolean tryLock();
//超時的獲取鎖,當前執行緒在三種情況下會返回:
//1、執行緒在超時時間內獲得了鎖。
//2、超時時間結束,返回false。
//3、執行緒在超時時間內被中斷。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
public class ReentrantLockTest {
static Lock lock = new ReentrantLock();
/**
* 測試非阻塞的獲取鎖
* 避免死鎖 ,如果在規定的時間內不能獲得鎖,自動釋放已經獲得的其它鎖。
* @param time
* @param unit
* @throws InterruptedException
*/
public static void testTryLock(long time ,TimeUnit unit) throws InterruptedException {
boolean tryLock = lock.tryLock(time, unit);
System.out.println(Thread.currentThread().getName()+":"+tryLock);
try {
Thread.currentThread().sleep(10000);
} finally {
// 如果獲取了鎖 釋放鎖 ,如果沒有獲得鎖 ,就釋放鎖 會報錯。
if (tryLock) {
lock.unlock();
}
}
}
//兩個執行緒同時去爭搶一個鎖,一個會獲得鎖,另一個會超時返回flase。
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
try {
testTryLock(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread b = new Thread(new Runnable() {
@Override
public void run() {
try {
testTryLock(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
a.setName("a");
b.setName("b");
a.start();
b.start();
}
}
在我的機器上輸出的結果是:
b:true
a:false
1.4 可中斷的鎖獲取操作
對應的java API操作是:
// 可中斷的獲取鎖,與lock方法不同的是該方法可以響應中斷,在獲取鎖的過程中可以中斷當前執行緒。
void lockInterruptibly() throws InterruptedException;
為什麼需要可中斷的所獲取操作 ?
內建鎖,擁有不可中斷的阻塞機制使得實現可取消的任務變得十分複雜。lockInterruptibly()方法能夠在獲取鎖的同時保持對中斷的響應,並且它包含於Lock中,因此不需要建立其它型別的不可中斷機制。
1.5 非塊結構的加鎖
在內建鎖中,鎖的獲取操作和釋放操作都是基於程式碼塊的。
鏈式加鎖(又稱鎖耦合)當遍歷或修改連結串列時,我們必須持有該節點上的這個鎖,直到獲取了下一個節點的鎖,只有這樣才能釋放前一個節點的鎖。
1.6 吞吐量
在java5中ReentrantLock比內建鎖的吞吐量要高出許多,但在java6中二者很接近。
1.7 公平性
ReentrantLock支援公平性的鎖和非公平性的鎖。
- 公平鎖 :執行緒按照它們的請求順序來獲得鎖。
- 非公平鎖:當一個執行緒請求鎖的時候,如果在發出請求的同時該鎖的狀態變為可用,那麼這個執行緒將跳過佇列中所有的等待執行緒二獲取鎖。
大多數情況下,非公平性的鎖效能好於公平性鎖的效能。
為什麼非公平性鎖的效能要優於公平性鎖呢?
在恢復一個被掛起的執行緒時與該執行緒真正開始執行之間存在嚴重的延遲。(說白了就是存在上下文切換的開銷,非公平鎖從另一個思路避免這種開銷。同樣我們也能想到偏向鎖,也是這樣一個思路:偏向程序會一直持有這個鎖,直到發生競爭才釋放掉。這個思路不也是為了減少上下文切換的開銷嗎?)
對於公平鎖而言,可輪詢的tryLock依然會“插隊”。
1.8 如何選擇synchronized 和 ReentrantLock ?
synchronized 是JVM的內建屬性,隨著jdk版本的提高可能會不斷被優化,而ReentrantLock是基於java類庫實現的,被優化的可能性不高。
ReentrantLock 可以作為一種高階工具,當synchronized無法滿足需求時使用ReentrantLock。例如:可定時的、可輪詢的、可中斷的鎖獲取操作;公平佇列以及非塊結構的鎖。