一.Lock和ReentrantLock
與內部加鎖機制不同,lock提供了無條件的,可輪詢的,定時的,可中斷的鎖獲取操作,所有的加鎖和解鎖的方法都是顯示的。
lock的鎖更加複雜:鎖必須在finally中釋放,另一個方面,如果鎖守護的代碼在try塊之外拋出了異常,它永遠不會被釋放;如果對象能夠被置於不一致的狀態,可能需要額外的try-catch,或try-finally。
1.可輪詢的和可定時的鎖請求
可定時的和可輪詢的鎖獲取模式,是由tryLock方法實現。與無條件的鎖獲取相比它它具有更完善的錯誤恢復機制。內部鎖中唯一的恢復方法是重行啓動程序,唯一的構建方法是構建程序時不要出錯。可輪詢和可定時的鎖提供了另一個鎖選擇:可以避免死鎖的發生。
如果你不能獲取所有需要的鎖,那麼使用可定時的和可輪詢的獲取方式使你能夠重新拿到控制權,它會釋放你已經獲得的這些鎖,然後再重新嘗試。(至少會記錄這個失敗,或採取其他措施)。
對於那些具有時間限制的活動,當它們調用了阻塞方法,定時鎖能夠在時間預算內設定響應的超時。如果活動在時間內沒能獲得結果,這個機制使程序能夠提前返回。使用內部鎖一旦開始請求,鎖就不能停止了。
2.可中斷的鎖獲取操作
可中斷的鎖獲取操作允許在可取消的活動中使用。當你正在響應中斷的是,lockInterruptibly方法使你能獲得鎖,並且由於它是內置Lock的,因此你不必在創建其他種類不可中斷的阻塞機制。
使用tryLock避免順序死鎖
public boolean transferMoney(final Account fromAccount, final Account toAccount, final DollarAmount amount,long timeout, TimeUnit unit) throws InterruptedException {
long fixedDelay = 123;
long randMod = 456;
long stopTime = System.nanoTime() + unit.toNanos(timeout);
while (true) {
if (fromAccount.getLock().tryLock()) { // 如果不能獲得鎖就返回重試
try {
if (toAccount.getLock().tryLock()) {
try {
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new RuntimeException();
} else {
fromAccount.debit(amount);
toAccount.credit(amount);
}
} finally {
toAccount.getLock().unlock();
}
}
} finally {
fromAccount.getLock().unlock();
}
}
if (System.nanoTime() > stopTime) { // 重試了指定次數仍然無法獲得鎖則返回失敗
return false;
}
Thread.sleep(fixedDelay + new Random().nextLong() % randMod);
}
}
使用預定時間的鎖
public boolean trySendOneSharedLine(String message, long timeout, TimeUnit unit) throws InterruptedException {
long nanosToLock = unit.toNanos(timeout) - estimatedNanosToSend(message);
if (lock.tryLock(nanosToLock, TimeUnit.NANOSECONDS)) {
return false;
}
try {
return sendOnSharedLined(message);
}finally {
lock.unlock();
}
}
可中斷的鎖獲取請求
public boolean sendOnSharedLine(String message)throws InterruptedException{
lock.lockInterruptibly();
try{
return cancellableSendOnSharedLine(message);
}finally{
lock.unlock();
}
}
private boolean cancellableSendOnSharedLine(String message)throws InterruptedException{
...
}
3.非塊結構的鎖
在內部鎖中,獲取和釋放這樣的行爲是塊結構的-總是在其獲得的相同的基本程序塊中釋放,而不考慮控制權是如何退出阻塞塊的。
在鏈表中,我們可以通過每個鏈表節點應用分離鎖來減小鎖的粒度,給定節點的鎖守護鏈接的指針,所以要遍歷或修改鏈表,我們必須得到這個鎖,並持有它直到我們獲得了下一個鎖;這之後我盟才能釋放前一個鎖。這項技術被稱作連式鎖,或者鎖聯接。
二.對性能的考量
當ReentrantLock被加入到java5.0時它提供的競爭上的性能要遠遠優於內部鎖。
1.公平性
ReentrantLock構造函數提供了倆種公平性選擇:創建非公平鎖(默認)或者公平鎖。
線程按順序請求獲得公平鎖,非公平鎖允許“闖入”:當請求到這樣的鎖時,如果鎖的狀態變爲可用,線程的請求可以在等待線程的隊列中向前跳躍獲得該鎖。(Semaphore同樣提供了公平的和非公平的獲取順序)。
4.在synchronized和ReentrantLock之間進行選擇
內部鎖相比於顯示鎖有很大的優勢。它更加簡潔。ReentrantLock絕對是最危險的同步工具。內部鎖與ReentrantLock相比,還有另外一個優點:線程轉儲能夠顯示哪些個調用框架獲得了哪些鎖,並能夠識別發生了死鎖的那些線程。
java中提供了管理和調試接口,可以使用這個接口進行註冊,並通過其他管理和調試接口,從線程轉儲中得到ReentrantLock的加鎖信息。
未來的性能改進可能更傾向於synchronized;因爲它是內置於JVM的,它能夠進行優化。
在內部鎖不能足夠使用時,ReentrantLock才被作爲更高級的工具使用。
5.讀寫鎖
ReentrantLock實現了標準互斥鎖;一次最多隻有一個線程能夠持有相同ReentrantLock。但是互斥鎖過分的限制了併發性。
讀寫鎖:一個資源能夠被多個讀者訪問,或者被一個寫者訪問,兩者不能同時進行。與Lock一樣ReadWriteLock允許多種實現,造成了性能、調度保證、獲取優先、公平性、以及加鎖語義方面不盡相同。
在頻繁讀的情況下讀寫鎖能夠改進性能,在其他情況下允許的情況下比獨佔鎖要稍差一些,這歸根於它更大的複雜性。
讀取和寫入的互動有多種實現。ReadWriteLock的一些實現選擇如下:
- 釋放優先。當寫者釋放寫入鎖,並且讀者和寫者都排在隊列中,應該選擇哪一個,讀者寫者,還是先請求的那個?
- 讀者闖入。如果鎖由讀者獲得,但有寫者正在等待,那麼寫到達的寫者應該被授予讀取的權利麼?還是應該等待?允許讀者闖入到寫者之前提高了併發性,但是卻帶來了寫者飢餓的問題。
- 重進入。讀取鎖和寫入鎖允許重進入嗎?
- 降級。如果線程持有寫入的鎖,它能夠在不釋放鎖的情況下獲取讀取的鎖嗎?這可能造成寫者“降級”爲一個讀取鎖,同時不允許其它寫着修改這個被守護的資源
- 升級。讀取鎖能夠優先於其他的讀者和寫者升級爲一個寫入鎖麼?大多數讀寫鎖時間並不支持升級,因爲在沒有顯示的升級的操作的情況下,很容易造成死鎖。(如果倆個讀者同時試圖升級到同一個寫入鎖,並不釋放讀取鎖)。
ReentrantReadWriteLock爲倆個鎖提供了可重進入的加鎖語義。它能夠被構造爲非公平(默認)或者是公平的。在公平的鎖中,選擇權交給等待時間最長的線程;如果鎖由讀者獲得,而一個線程請求寫入鎖,那麼不再允許讀者獲得讀取鎖。直到寫者被受理,並且已經釋放了寫入鎖,在非公平鎖中線程允許訪問順序是不定的。由寫者降級爲讀者是允許的;從讀者升級爲寫者是不允許的。
用讀寫鎖包裝的map
public class ReadWriteMap <K,V> {
private final Map<K,V> map;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock r = lock.readLock();
private final Lock w = lock.writeLock();
public ReadWriteMap(Map<K, V> map){
this.map = map;
}
public void put(K key, V value){
w.lock();
try{
return map.put(key, value);
}finally{
w.unlock();
}
}//remove(), putAll(), clear()也完全類似
public V get(Object key){
r.lock();
try{
return map.get(key);
}finally{
r.unlock();
}
}//對於其他的只讀Map方法也完全類似
}