并發(fā)鎖之 Lock 接口
1. 前言
本節(jié)內(nèi)容主要是對 Java 并發(fā)鎖之 Lock 接口進行介紹,Lock 是類似于 synchronized 的另外一種鎖的使用,那么本節(jié)我們會對 Lock 進行詳細的介紹,主要知識點如下:
- Lock 接口的介紹,這是我們開始認識 Lock 的敲門磚,本節(jié)課程的基礎(chǔ)知識;
- Lock 接口相比于 synchronized 關(guān)鍵字的優(yōu)點,這也是我們學(xué)習(xí) Lock 接口的意義所在;
- Lock 接口的常用方法介紹,了解 Lock 接口中的常用方法,是本節(jié)內(nèi)容的核心知識點。
Lock 是一個接口,并非一個實現(xiàn)類,本節(jié)內(nèi)容主要對 Lock 接口進行一個意義、結(jié)構(gòu)及方法的介紹,為后續(xù)講解 Lock 接口的實現(xiàn)類常用鎖奠定一個扎實的基礎(chǔ)。
2. Lock 接口的介紹
Lock 接口的誕生:在 Java 中鎖的實現(xiàn)可以由 synchronized 關(guān)鍵字來完成,但在 Java5 之后,出現(xiàn)了一種新的方式來實現(xiàn),即 Lock 接口。
誕生的意義:Lock 接口支持那些語義不同(重入、公平等)的鎖規(guī)則,可以在非阻塞式結(jié)構(gòu)的上下文(包括 hand-over-hand 和鎖重排算法)中使用這些規(guī)則。主要的實現(xiàn)是 ReentrantLock。對于 ReentrantLock,后續(xù)有專門的小節(jié)進行講解。
JDK 1.5 前的 synchronized:在多線程的情況下,當(dāng)一段代碼被 synchronized 修飾之后,同一時刻只能被一個線程訪問,其他線程都必須等到該線程釋放鎖之后才能有機會獲取鎖訪問這段代碼。
Lock 接口: 實現(xiàn)提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。此實現(xiàn)允許更靈活的結(jié)構(gòu),可以具有差別很大的屬性,可以支持多個相關(guān)的 Condition 對象。
Lock 相對于 synchronized 關(guān)鍵字而言更加靈活,你可以自由得選擇你想要加鎖的地方。當(dāng)然更高的自由度也帶來更多的責(zé)任。
使用示例:我們通常會在 try catch 模塊中使用 Lock 關(guān)鍵字,在 finally 模塊中釋放鎖。
Lock lock = new ReentrantLock(); //通過子類進行創(chuàng)建,此處以ReentrantLock進行舉例
lock.lock(); //加鎖
try {
// 對上鎖的邏輯進行操作
} finally {
lock.unlock(); //釋放鎖
}
3. Lock 接口與 synchronized 關(guān)鍵字的區(qū)別
- 實現(xiàn):synchronized 關(guān)鍵字基于 JVM 層面實現(xiàn),JVM 控制鎖的獲取和釋放。Lock 接口基于 JDK 層面,手動進行鎖的獲取和釋放;
- 使用:synchronized 關(guān)鍵字不用手動釋放鎖,Lock 接口需要手動釋放鎖,在 finally 模塊中調(diào)用 unlock 方法;
- 鎖獲取超時機制:synchronized 關(guān)鍵字不支持,Lock 接口支持;
- 獲取鎖中斷機制:synchronized 關(guān)鍵字不支持,Lock 接口支持;
- 釋放鎖的條件:synchronized 關(guān)鍵字在滿足占有鎖的線程執(zhí)行完畢,或占有鎖的線程異常退出,或占有鎖的線程進入 waiting 狀態(tài)才會釋放鎖。Lock 接口調(diào)用 unlock 方法釋放鎖;
- 公平性:synchronized 關(guān)鍵字為非公平鎖。Lock 接口可以通過入?yún)⒆孕性O(shè)置鎖的公平性。
4. Lock 接口相比 synchronized 關(guān)鍵字的優(yōu)勢
我們通過兩個個案例分析來了解 Lock 接口的優(yōu)勢所在。
案例 1 :在使用 synchronized 關(guān)鍵字的情形下,假如占有鎖的線程由于要等待 IO 或者其他原因(比如調(diào)用 sleep 方法)被阻塞了,但是又沒有釋放鎖,那么其他線程就只能一直等待,別無他法。這會極大影響程序執(zhí)行效率。
案例 1 分析:該案例體現(xiàn)了 synchronized 的缺陷,當(dāng)線程被占有時,其他線程會陷入無條件的長期等待。這是非常可怕的,因為系統(tǒng)資源有限,最終可能導(dǎo)致系統(tǒng)崩潰。
案例 1 解決:Lock 接口中的 tryLock (long time, TimeUnit unit) 方法或者響應(yīng)中斷 lockInterruptibly () 方法,能夠解決這種長期等待的情況。
案例 2 :我們知道,當(dāng)多個線程讀寫文件時,讀操作和寫操作會發(fā)生沖突現(xiàn)象,寫操作和寫操作也會發(fā)生沖突現(xiàn)象,但是讀操作和讀操作不會發(fā)生沖突現(xiàn)象。
但是如果采用 synchronized 關(guān)鍵字實現(xiàn)同步的話,就會導(dǎo)致一個問題,即當(dāng)多個線程都只是進行讀操作時,也只有一個線程可以進行讀操作,其他線程只能等待鎖的釋放而無法進行讀操作。
案例 2 分析:該案例體現(xiàn)了 synchronized 的缺陷,悲觀鎖的缺陷。我們說過,如果只是讀操作,沒有增刪改操作的話,多線程環(huán)境下無需加鎖。但是這種情況下,如果在同一時間多個線程進行讀操作,synchronized 會 block 其他的讀操作,這是不合理的。
案例 2 解決:Lock 接口家族也可以解決這種情況,后續(xù)我們會對 ReadWriteLock 接口的一個子類 ReentrantReadWriteLock 進行講解。
總結(jié):Lock 接口實現(xiàn)提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作,能夠解決 synchronized 不能夠避免的問題。
5. Lock 接口的常用方法
我們來簡單的看下,JDK 中 Lock 接口的源碼中所包含的方法:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
方法介紹:
- void lock():獲取鎖。如果鎖不可用,出于線程調(diào)度目的,將禁用當(dāng)前線程,并且在獲得鎖之前,該線程將一直處于休眠狀態(tài);
- void lockInterruptibly():如果當(dāng)前線程未被中斷,則獲取鎖;
- boolean tryLock():僅在調(diào)用時鎖為空閑狀態(tài)才獲取該鎖。如果鎖可用,則獲取鎖,并立即返回值 true。如果鎖不可用,則此方法將立即返回值 false;
- boolean tryLock(long time, TimeUnit unit):如果鎖在給定的等待時間內(nèi)空閑,并且當(dāng)前線程未被中斷,則獲取鎖;
- void unlock():釋放鎖。在等待條件前,鎖必須由當(dāng)前線程保持。調(diào)用 Condition.await () 將在等待前以原子方式釋放鎖,并在等待返回前重新獲取鎖;
- Condition newCondition():返回綁定到此 Lock 實例的新 Condition 實例。
Tips:對 Lock 接口方法的使用,我們必須基于子類進行 Lock 的創(chuàng)建來展示,由于目前我們還未接觸 Lock 接口的實現(xiàn)子類,此處只做方法的介紹。后續(xù)對 ReentrantLock 進行講解時,會進行深入講解。
6. 小結(jié)
本節(jié)主要是對 Lock 接口的常用方法進行了介紹,為本節(jié)內(nèi)容的核心知識。除了方法的介紹外,本節(jié)內(nèi)容不容忽視的一個重點內(nèi)容是 synchronized 關(guān)鍵字與 Lock 接口的區(qū)別,以及 Lock 接口的優(yōu)勢所在。
掌握本節(jié)內(nèi)容,有助于同學(xué)對后續(xù)實現(xiàn)類鎖的學(xué)習(xí),為后續(xù)的學(xué)習(xí)奠定了良好的基礎(chǔ)。