聊聊ReentrantLock 中的四個坑!
作者 | 王磊
來源 | Java中文社群(ID:javacn666)
轉(zhuǎn)載請聯(lián)系授權(quán)(微信ID:GG_Stone)
JDK 1.5 之前 synchronized 的性能是比較低的,但在 JDK 1.5 中,官方推出一個重量級功能 Lock,一舉改變了 Java 中鎖的格局。JDK 1.5 之前當(dāng)我們談到鎖時,只能使用內(nèi)置鎖 synchronized,但如今我們鎖的實現(xiàn)又多了一種顯式鎖 Lock。
本文咱們重點來看 Lock。
Lock 簡介
Lock 是一個頂級接口,它的所有方法如下圖所示:
它的子類列表如下:
我們通常會使用 ReentrantLock 來定義其實例,它們之間的關(guān)聯(lián)如下圖所示:
“PS:Sync 是同步鎖的意思,F(xiàn)airSync 是公平鎖,NonfairSync 是非公平鎖。
ReentrantLock 使用
學(xué)習(xí)任何一項技能都是先從使用開始的,所以我們也不例外,咱們先來看下 ReentrantLock 的基礎(chǔ)使用:
- publicclass LockExample {
- // 創(chuàng)建鎖對象
- privatefinal ReentrantLock lock = new ReentrantLock();
- public void method() {
- // 加鎖操作
- lock.lock();
- try {
- // 業(yè)務(wù)代碼......
- } finally {
- // 釋放鎖
- lock.unlock();
- }
- }
- }
ReentrantLock 在創(chuàng)建之后,有兩個關(guān)鍵性的操作:
- 加鎖操作:lock()
- 釋放鎖操作:unlock()
ReentrantLock 中的坑
1.ReentrantLock 默認為非公平鎖
很多人會認為(尤其是新手朋友),ReentrantLock 默認的實現(xiàn)是公平鎖,其實并非如此,ReentrantLock 默認情況下為非公平鎖(這主要是出于性能方面的考慮),比如下面這段代碼:
- import java.util.concurrent.locks.ReentrantLock;
- publicclass LockExample {
- // 創(chuàng)建鎖對象
- privatestaticfinal ReentrantLock lock = new ReentrantLock();
- public static void main(String[] args) {
- // 定義線程任務(wù)
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- // 加鎖
- lock.lock();
- try {
- // 打印執(zhí)行線程的名字
- System.out.println("線程:" + Thread.currentThread().getName());
- } finally {
- // 釋放鎖
- lock.unlock();
- }
- }
- };
- // 創(chuàng)建多個線程
- for (int i = 0; i < 10; i++) {
- new Thread(runnable).start();
- }
- }
- }
以上程序的執(zhí)行結(jié)果如下:
從上述執(zhí)行的結(jié)果可以看出,ReentrantLock 默認情況下為非公平鎖。因為線程的名稱是根據(jù)創(chuàng)建的先后順序遞增的,所以如果是公平鎖,那么線程的執(zhí)行應(yīng)該是有序遞增的,但從上述的結(jié)果可以看出,線程的執(zhí)行和打印是無序的,這說明 ReentrantLock 默認情況下為非公平鎖。
想要將 ReentrantLock 設(shè)置為公平鎖也很簡單,只需要在創(chuàng)建 ReentrantLock 時,設(shè)置一個 true 的構(gòu)造參數(shù)就可以了,如下代碼所示:
- import java.util.concurrent.locks.ReentrantLock;
- publicclass LockExample {
- // 創(chuàng)建鎖對象(公平鎖)
- privatestaticfinal ReentrantLock lock = new ReentrantLock(true);
- public static void main(String[] args) {
- // 定義線程任務(wù)
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- // 加鎖
- lock.lock();
- try {
- // 打印執(zhí)行線程的名字
- System.out.println("線程:" + Thread.currentThread().getName());
- } finally {
- // 釋放鎖
- lock.unlock();
- }
- }
- };
- // 創(chuàng)建多個線程
- for (int i = 0; i < 10; i++) {
- new Thread(runnable).start();
- }
- }
- }
以上程序的執(zhí)行結(jié)果如下:
從上述結(jié)果可以看出,當(dāng)我們顯式的給 ReentrantLock 設(shè)置了 true 的構(gòu)造參數(shù)之后,ReentrantLock 就變成了公平鎖,線程獲取鎖的順序也變成有序的了。
其實從 ReentrantLock 的源碼我們也可以看出它究竟是公平鎖還是非公平鎖,ReentrantLock 部分源碼實現(xiàn)如下:
- public ReentrantLock() {
- sync = new NonfairSync();
- }
- public ReentrantLock(boolean fair) {
- sync = fair ? new FairSync() : new NonfairSync();
- }
從上述源碼中可以看出,默認情況下 ReentrantLock 會創(chuàng)建一個非公平鎖,如果在創(chuàng)建時顯式的設(shè)置構(gòu)造參數(shù)的值為 true 時,它就會創(chuàng)建一個公平鎖。
2.在 finally 中釋放鎖
使用 ReentrantLock 時一定要記得釋放鎖,否則就會導(dǎo)致該鎖一直被占用,其他使用該鎖的線程則會永久的等待下去,所以我們在使用 ReentrantLock 時,一定要在 finally 中釋放鎖,這樣就可以保證鎖一定會被釋放。
反例
- import java.util.concurrent.locks.ReentrantLock;
- publicclass LockExample {
- // 創(chuàng)建鎖對象
- privatestaticfinal ReentrantLock lock = new ReentrantLock();
- public static void main(String[] args) {
- // 加鎖操作
- lock.lock();
- System.out.println("Hello,ReentrantLock.");
- // 此處會報異常,導(dǎo)致鎖不能正常釋放
- int number = 1 / 0;
- // 釋放鎖
- lock.unlock();
- System.out.println("鎖釋放成功!");
- }
- }
以上程序的執(zhí)行結(jié)果如下:
從上述結(jié)果可以看出,當(dāng)出現(xiàn)異常時鎖未被正常釋放,這樣就會導(dǎo)致其他使用該鎖的線程永久的處于等待狀態(tài)。
正例
- import java.util.concurrent.locks.ReentrantLock;
- publicclass LockExample {
- // 創(chuàng)建鎖對象
- privatestaticfinal ReentrantLock lock = new ReentrantLock();
- public static void main(String[] args) {
- // 加鎖操作
- lock.lock();
- try {
- System.out.println("Hello,ReentrantLock.");
- // 此處會報異常
- int number = 1 / 0;
- } finally {
- // 釋放鎖
- lock.unlock();
- System.out.println("鎖釋放成功!");
- }
- }
- }
以上程序的執(zhí)行結(jié)果如下:
從上述結(jié)果可以看出,雖然方法中出現(xiàn)了異常情況,但并不影響 ReentrantLock 鎖的釋放操作,這樣其他使用此鎖的線程就可以正常獲取并運行了。
3.鎖不能被釋放多次
lock 操作的次數(shù)和 unlock 操作的次數(shù)必須一一對應(yīng),且不能出現(xiàn)一個鎖被釋放多次的情況,因為這樣就會導(dǎo)致程序報錯。
反例
一次 lock 對應(yīng)了兩次 unlock 操作,導(dǎo)致程序報錯并終止執(zhí)行,示例代碼如下:
- import java.util.concurrent.locks.ReentrantLock;
- publicclass LockExample {
- // 創(chuàng)建鎖對象
- privatestaticfinal ReentrantLock lock = new ReentrantLock();
- public static void main(String[] args) {
- // 加鎖操作
- lock.lock();
- // 第一次釋放鎖
- try {
- System.out.println("執(zhí)行業(yè)務(wù) 1~");
- // 業(yè)務(wù)代碼 1......
- } finally {
- // 釋放鎖
- lock.unlock();
- System.out.println("鎖釋鎖");
- }
- // 第二次釋放鎖
- try {
- System.out.println("執(zhí)行業(yè)務(wù) 2~");
- // 業(yè)務(wù)代碼 2......
- } finally {
- // 釋放鎖
- lock.unlock();
- System.out.println("鎖釋鎖");
- }
- // 最后的打印操作
- System.out.println("程序執(zhí)行完成.");
- }
- }
以上程序的執(zhí)行結(jié)果如下:
從上述結(jié)果可以看出,執(zhí)行第 2 個 unlock 時,程序報錯并終止執(zhí)行了,導(dǎo)致異常之后的代碼都未正常執(zhí)行。
4.lock 不要放在 try 代碼內(nèi)
在使用 ReentrantLock 時,需要注意不要將加鎖操作放在 try 代碼中,這樣會導(dǎo)致未加鎖成功就執(zhí)行了釋放鎖的操作,從而導(dǎo)致程序執(zhí)行異常。
反例
- import java.util.concurrent.locks.ReentrantLock;
- publicclass LockExample {
- // 創(chuàng)建鎖對象
- privatestaticfinal ReentrantLock lock = new ReentrantLock();
- public static void main(String[] args) {
- try {
- // 此處異常
- int num = 1 / 0;
- // 加鎖操作
- lock.lock();
- } finally {
- // 釋放鎖
- lock.unlock();
- System.out.println("鎖釋鎖");
- }
- System.out.println("程序執(zhí)行完成.");
- }
- }
以上程序的執(zhí)行結(jié)果如下:
從上述結(jié)果可以看出,如果將加鎖操作放在 try 代碼中,可能會導(dǎo)致兩個問題:
- 未加鎖成功就執(zhí)行了釋放鎖的操作,從而導(dǎo)致了新的異常;
- 釋放鎖的異常會覆蓋程序原有的異常,從而增加了排查問題的難度。
總結(jié)
本文介紹了 Java 中的顯式鎖 Lock 及其子類 ReentrantLock 的使用和注意事項,Lock 在 Java 中占據(jù)了鎖的半壁江山,但在使用時卻要注意 4 個問題:
- 默認情況下 ReentrantLock 為非公平鎖而非公平鎖;
- 加鎖次數(shù)和釋放鎖次數(shù)一定要保持一致,否則會導(dǎo)致線程阻塞或程序異常;
- 加鎖操作一定要放在 try 代碼之前,這樣可以避免未加鎖成功又釋放鎖的異常;
- 釋放鎖一定要放在 finally 中,否則會導(dǎo)致線程阻塞。