併發程式設計學習筆記之顯示鎖(十一)
ReentrantLock(重進入鎖)並不是作為內部鎖(synchronized)機制的替代,而是當內部鎖被證明受到侷限時,提供可選擇的高階特性.
1. Lock 和 ReentrantLock
Lock介面:
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
與內部加鎖機制不同,Lock提供了無條件的、可輪詢的、定時的、可中斷的鎖獲取操作,所有加鎖和解鎖的方法都是顯示的.
Lock的實現必須提供具有與內部加鎖相同的記憶體可見性的語義.但是加鎖的語義、排程演算法、順序保證,效能特性這些可以不同.
ReentrantLock實現了Lock介面,提供了與synchronized相同的互斥和記憶體可見性的保證.
獲得ReentrantLock的鎖與進入synchronized塊有著相同的記憶體語義,釋放ReentrantLock鎖與退出synchronized塊有相同的記憶體語義.
ReentrantLock提供了與synchronized一樣的可重入加鎖的語義.ReentrantLock支援Lock介面定義的所有獲取鎖的模式.
一句話synchronized能做的,ReentrantLock都能做,但是ReentrantLock為處理不可用的鎖提供了更多靈活性(好吧,ReentrantLock寫起來比較麻煩)
為什麼要使用顯示鎖
內部鎖在大部分情況下都能很好地工作,但是有一些功能上的侷限--不能中斷那些正在等待獲取鎖的執行緒,並且在請求鎖失敗的情況下,必須無限等待.
內部鎖必須在獲取他們的程式碼塊中被釋放:這很好地簡化了程式碼,與異常處理機制能夠良好的互動,但是在某些情況下,一個更靈活的加鎖機制提供了更好的活躍度和效能.
public class LockTest { Lock lock = new ReentrantLock(); public void testLock(){ lock.Lock(); try { // 需要加鎖的程式碼.. }finally { lock.unlock(); } } }
這個模式在某種程度上比使用內部鎖更加複雜:鎖必須在finally塊中釋放.
另一方面,如果鎖守護的程式碼在try塊之外丟擲了異常,它將永遠都不會被釋放了;
如果物件能夠被置於不一致的狀態,可能需要額外的try-catch,或try-finally塊.
顯示的lock的缺點
使用lock之後必須unlock釋放鎖,這也是ReentrantLock不能完全替代synchronized的原因.
它更加危險,因為當程式的控制權離開了守護的塊時,不會自動清除鎖.
1.1 可輪詢和可定時的鎖請求
可定時的與可輪詢的鎖獲取模式,是由tryLock方法實現,與無條件的鎖獲取相比,它具有更完善的錯誤恢復機制.
使用內部鎖,發生死鎖時唯一的恢復方法是重啟程式,唯一的預防方法是在構建程式時不要出錯,所以不可能允許不一致的鎖順序.
可定時的與可輪詢的鎖提供了另一個選擇,可以規避死鎖的發生.
使用方式:
public class LockSample {
//建立一個鎖的例項
Lock lock = new ReentrantLock();
public void methodA(){
lock.lock();
try {
System.out.println("執行了方法A");
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void methodB(){
lock.lock();
try {
System.out.println("執行了方法B");
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String [] args){
LockSample lockSample = new LockSample();
lockSample.methodA();
//methodB()方法必須在鎖可用的時候才會執行
lockSample.methodB();
}
}
使用tryLock能解決第九篇部落格死鎖,提到過的動態的順序死鎖問題.
public class LockTest {
public static void main(String [] args){
LockTest lockTest = new LockTest();
Account fromAccount = new Account();
Account toAccount = new Account();
Account account = new Account();
//開啟一個新執行緒,獲取兩個使用者的鎖,這個方法是假設,物件的鎖已經被獲得用的.
new Thread(){
@Override
public void run(){
//這兩個方法的內部實現就是Thread.sleep()將程式碼阻塞住.
fromAccount.credit(account);
toAccount.dedit(account);
}
}.start();
lockTest.transferMoney(fromAccount,toAccount,account);
}
public void transferMoney(Account fromAccount,Account toAccount,Account account){
while(true){
// lock.tryLock()返回一個布林值,告訴你當前的鎖是否可用,如果可用往下走
if(fromAccount.lock.tryLock()){
try {
if (toAccount.lock.tryLock()){
try {
//走到這裡,證明兩個鎖都可用,可以進行轉賬操作.
fromAccount.credit(account);
toAccount.dedit(account);
}finally {
toAccount.lock.unlock();
}
}
}finally {
fromAccount.lock.unlock();
}
}
}
}
}
Account的內部實現:
public class Account {
public Lock lock = new ReentrantLock();
public void credit(Account account) {
lock.lock();
try {
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
public void dedit(Account account) {
lock.lock();
try {
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
定時鎖可以在時間預算內設定相應的超時,如果活動子啊期待的時間內沒能獲得結果,這個機制是程式能夠提前返回.
而使用內部鎖一旦開始請求,鎖就不能停止了,所以內部鎖為實現具有時限的活動帶來了風險.
.tryLock方法還有一個過載版本,可以設定等待的時間:
lock.tryLock(4, TimeUnit.SECONDS)
1.2 可中斷的鎖獲取操作
lock.lockInterruptibly上的鎖,是可以響應中斷的:
public class LockSample {
//建立一個鎖的例項
Lock lock = new ReentrantLock();
public void testInterruptibly(){
try {
lock.lockInterruptibly();
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void test(){
System.out.println("lock.tryLock() = " + lock.tryLock());
try {
System.out.println("lock.tryLock(4,TimeUnit.SECONDS) = " + lock.tryLock(4, TimeUnit.SECONDS));;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void methodA(){
lock.lock();
try {
System.out.println("執行了方法A");
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void methodB(){
lock.lock();
try {
System.out.println("執行了方法B");
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String [] args){
Long startTime = System.nanoTime();
LockSample lockSample = new LockSample();
Thread thread = Thread.currentThread();
new Thread(){
@Override
public void run(){
try {
//休眠兩秒,執行中斷
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
//這裡本來是休眠5秒的,因為上面直接中斷了,可以看下面的endtime是兩秒,證明了可以被中斷
lockSample.testInterruptibly();
Long endTime = startTime - System.nanoTime();
System.out.println("endTime = " + endTime);
}
}
2. 對效能的考量
ReentrantLock提供的競爭上的效能要遠遠優於內部鎖.
對於同步原語而言,競態時的效能是可伸縮性的關鍵:若果有越多的資源花費在鎖的管理和排程上,那程式執行的時間就越少.
在Java5.0中,ReentrantLock相比於synchronized能給吞吐量帶來相當不錯的提升,但是在Java6中,這兩者非常接近.
也就是說之前選擇顯示鎖,還有效能方面的考量,但是現在顯示鎖和synchronized已經差不多了.
3. 公平性
ReentrantLock建構函式提供了兩種公平性的選擇:
- 建立非公平鎖(預設)
- 公平鎖
公平鎖:如果鎖已經被其他執行緒佔有,新的請求執行緒會加入到等待佇列,或者已經有一些執行緒在等待鎖了;
非公平鎖: 非公平鎖允許闖入,當請求這樣的鎖時,如果鎖的狀態變為可用,執行緒的請求可以在等待執行緒的佇列中向前跳躍,獲得該鎖.(Semaphore同樣提供了公平和非公平的獲取順序).在非公平的鎖中,執行緒只有當鎖正在被佔用時才會等待.
為什麼要使用不公平鎖
當發生加鎖的時候,公平會因為掛起和重新開始執行緒的代價帶來巨大的效能開銷.
在多數情況下,非公平鎖的優勢超過了公平的排隊.
在競爭激烈的情況下,闖入鎖比公平鎖效能好的原因之一是:掛起的執行緒重新開始,與它真正開始執行,兩者之間會產生嚴重的延遲.
比較公平鎖和非公平鎖,使用的例子:
假設執行緒A持有一個鎖,執行緒B請求該鎖.因為此時鎖正在使用中,執行緒B被掛起,當A釋放鎖後,B重新開始.與此同時,如果C請求鎖,那麼C得到了很好的機會獲得這個鎖,使用它,並且甚至可能在B被喚醒前就已經釋放該鎖了.
在這樣的情況下,各方面都獲得了成功:B並沒有比其他任何執行緒晚得到鎖,C更早的得到了鎖,吞吐量得到了改進.
如果持有鎖的時間相對較長,或者請求鎖的平均時間間隔較長,那麼使用公平鎖是比較好的.
4. 在synchronized和ReentrantLock之間進行選擇
在內部鎖不能夠滿足使用時,ReentrantLock才被作為更高階的工具,當你需要以下高階特性時,才應該使用:
可定時的、可輪詢的與可中斷的鎖獲取操作,公平佇列,或者非塊結構的鎖,否則,請使用synchronized.
5. 讀-寫鎖
讀-寫鎖:一個資源能夠被多個讀者訪問,或者被一個寫者訪問,兩者不能同時進行.
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
ReadWriteLock暴露了兩個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 V put(K key,V value){
w.lock();
try {
return map.put(key,value);
}finally {
w.unlock();
}
}
//remove(),putAll(),clear()使用w.lock
public V get(Object key){
r.lock();
try {
return map.get(key);
}finally {
r.unlock();
}
}
//其他的只讀map使用r.lock
}
總結
顯示的Lock與內部鎖相比提供了一些擴充套件的特性,包括處理不可用的鎖時更好的靈活性,以及對佇列行為更好的控制,但是ReentrantLock不能完全替代synchronized;只有當你需要synchronized沒能提供的特性時才應該使用.
讀-寫鎖允許多個讀者併發訪問被守護的物件,當訪問多為讀取資料結構的時候,它具有改進可伸縮性的能力.