Java中ReentrantLock4種常見(jiàn)的坑
前言
JDK 1.5 之前 synchronized 的性能是比較低的,但在 JDK 1.5 中,官方推出一個(gè)重量級(jí)功能 Lock,一舉改變了 Java 中鎖的格局。JDK 1.5 之前當(dāng)我們談到鎖時(shí),只能使用內(nèi)置鎖 synchronized,但如今我們鎖的實(shí)現(xiàn)又多了一種顯式鎖 Lock。
前面的文章我們已經(jīng)介紹了 synchronized,詳見(jiàn)以下列表:
《淺談synchronized加鎖this和class的區(qū)別》
《Java中的synchronized 優(yōu)化方法之鎖膨脹機(jī)制》
《Java中synchronized 的4個(gè)優(yōu)化技巧》
所以本文咱們重點(diǎn)來(lái)看 Lock。
Lock 簡(jiǎn)介
Lock 是一個(gè)頂級(jí)接口,它的所有方法如下圖所示:
它的子類(lèi)列表如下:
我們通常會(huì)使用 ReentrantLock 來(lái)定義其實(shí)例,它們之間的關(guān)聯(lián)如下圖所示:
PS:Sync 是同步鎖的意思,F(xiàn)airSync 是公平鎖,NonfairSync 是非公平鎖。
ReentrantLock 使用
學(xué)習(xí)任何一項(xiàng)技能都是先從使用開(kāi)始的,所以我們也不例外,咱們先來(lái)看下 ReentrantLock 的基礎(chǔ)使用:
public class LockExample { // 創(chuàng)建鎖對(duì)象 private final ReentrantLock lock = new ReentrantLock(); public void method() { // 加鎖操作 lock.lock(); try { // 業(yè)務(wù)代碼...... } finally { // 釋放鎖 lock.unlock(); } } }
ReentrantLock 在創(chuàng)建之后,有兩個(gè)關(guān)鍵性的操作:
- 加鎖操作:lock()
- 釋放鎖操作:unlock()
ReentrantLock 中的坑
1.ReentrantLock 默認(rèn)為非公平鎖
很多人會(huì)認(rèn)為(尤其是新手朋友),ReentrantLock 默認(rèn)的實(shí)現(xiàn)是公平鎖,其實(shí)并非如此,ReentrantLock 默認(rèn)情況下為非公平鎖(這主要是出于性能方面的考慮),
比如下面這段代碼:
import java.util.concurrent.locks.ReentrantLock; public class LockExample { // 創(chuàng)建鎖對(duì)象 private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { // 定義線(xiàn)程任務(wù) Runnable runnable = new Runnable() { @Override public void run() { // 加鎖 lock.lock(); try { // 打印執(zhí)行線(xiàn)程的名字 System.out.println("線(xiàn)程:" + Thread.currentThread().getName()); } finally { // 釋放鎖 lock.unlock(); } } }; // 創(chuàng)建多個(gè)線(xiàn)程 for (int i = 0; i < 10; i++) { new Thread(runnable).start(); } } }
以上程序的執(zhí)行結(jié)果如下:
從上述執(zhí)行的結(jié)果可以看出,ReentrantLock 默認(rèn)情況下為非公平鎖。因?yàn)榫€(xiàn)程的名稱(chēng)是根據(jù)創(chuàng)建的先后順序遞增的,所以如果是公平鎖,那么線(xiàn)程的執(zhí)行應(yīng)該是有序遞增的,但從上述的結(jié)果可以看出,線(xiàn)程的執(zhí)行和打印是無(wú)序的,這說(shuō)明 ReentrantLock 默認(rèn)情況下為非公平鎖。
想要將 ReentrantLock 設(shè)置為公平鎖也很簡(jiǎn)單,只需要在創(chuàng)建 ReentrantLock 時(shí),設(shè)置一個(gè) true 的構(gòu)造參數(shù)就可以了,如下代碼所示:
import java.util.concurrent.locks.ReentrantLock; public class LockExample { // 創(chuàng)建鎖對(duì)象(公平鎖) private static final ReentrantLock lock = new ReentrantLock(true); public static void main(String[] args) { // 定義線(xiàn)程任務(wù) Runnable runnable = new Runnable() { @Override public void run() { // 加鎖 lock.lock(); try { // 打印執(zhí)行線(xiàn)程的名字 System.out.println("線(xiàn)程:" + Thread.currentThread().getName()); } finally { // 釋放鎖 lock.unlock(); } } }; // 創(chuàng)建多個(gè)線(xiàn)程 for (int i = 0; i < 10; i++) { new Thread(runnable).start(); } } }
以上程序的執(zhí)行結(jié)果如下:
從上述結(jié)果可以看出,當(dāng)我們顯式的給 ReentrantLock 設(shè)置了 true 的構(gòu)造參數(shù)之后,ReentrantLock 就變成了公平鎖,線(xiàn)程獲取鎖的順序也變成有序的了。
其實(shí)從 ReentrantLock 的源碼我們也可以看出它究竟是公平鎖還是非公平鎖,ReentrantLock 部分源碼實(shí)現(xiàn)如下:
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
從上述源碼中可以看出,默認(rèn)情況下 ReentrantLock 會(huì)創(chuàng)建一個(gè)非公平鎖,如果在創(chuàng)建時(shí)顯式的設(shè)置構(gòu)造參數(shù)的值為 true 時(shí),它就會(huì)創(chuàng)建一個(gè)公平鎖。
2.在 finally 中釋放鎖
使用 ReentrantLock 時(shí)一定要記得釋放鎖,否則就會(huì)導(dǎo)致該鎖一直被占用,其他使用該鎖的線(xiàn)程則會(huì)永久的等待下去,所以我們?cè)谑褂?ReentrantLock 時(shí),一定要在 finally 中釋放鎖,這樣就可以保證鎖一定會(huì)被釋放。
反例
import java.util.concurrent.locks.ReentrantLock; public class LockExample { // 創(chuàng)建鎖對(duì)象 private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { // 加鎖操作 lock.lock(); System.out.println("Hello,ReentrantLock."); // 此處會(huì)報(bào)異常,導(dǎo)致鎖不能正常釋放 int number = 1 / 0; // 釋放鎖 lock.unlock(); System.out.println("鎖釋放成功!"); } }
以上程序的執(zhí)行結(jié)果如下:
從上述結(jié)果可以看出,當(dāng)出現(xiàn)異常時(shí)鎖未被正常釋放,這樣就會(huì)導(dǎo)致其他使用該鎖的線(xiàn)程永久的處于等待狀態(tài)。
正例
import java.util.concurrent.locks.ReentrantLock; public class LockExample { // 創(chuàng)建鎖對(duì)象 private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { // 加鎖操作 lock.lock(); try { System.out.println("Hello,ReentrantLock."); // 此處會(huì)報(bào)異常 int number = 1 / 0; } finally { // 釋放鎖 lock.unlock(); System.out.println("鎖釋放成功!"); } } }
以上程序的執(zhí)行結(jié)果如下:
從上述結(jié)果可以看出,雖然方法中出現(xiàn)了異常情況,但并不影響 ReentrantLock 鎖的釋放操作,這樣其他使用此鎖的線(xiàn)程就可以正常獲取并運(yùn)行了。
3.鎖不能被釋放多次
lock 操作的次數(shù)和 unlock 操作的次數(shù)必須一一對(duì)應(yīng),且不能出現(xiàn)一個(gè)鎖被釋放多次的情況,因?yàn)檫@樣就會(huì)導(dǎo)致程序報(bào)錯(cuò)。
反例
一次 lock 對(duì)應(yīng)了兩次 unlock 操作,導(dǎo)致程序報(bào)錯(cuò)并終止執(zhí)行,示例代碼如下:
import java.util.concurrent.locks.ReentrantLock; public class LockExample { // 創(chuàng)建鎖對(duì)象 private static final 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 個(gè) unlock 時(shí),程序報(bào)錯(cuò)并終止執(zhí)行了,導(dǎo)致異常之后的代碼都未正常執(zhí)行。
4.lock 不要放在 try 代碼內(nèi)
在使用 ReentrantLock 時(shí),需要注意不要將加鎖操作放在 try 代碼中,這樣會(huì)導(dǎo)致未加鎖成功就執(zhí)行了釋放鎖的操作,從而導(dǎo)致程序執(zhí)行異常。
反例
import java.util.concurrent.locks.ReentrantLock; public class LockExample { // 創(chuàng)建鎖對(duì)象 private static final 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 代碼中,可能會(huì)導(dǎo)致兩個(gè)問(wèn)題:
- 未加鎖成功就執(zhí)行了釋放鎖的操作,從而導(dǎo)致了新的異常;
- 釋放鎖的異常會(huì)覆蓋程序原有的異常,從而增加了排查問(wèn)題的難度。
總結(jié)
本文介紹了 Java 中的顯式鎖 Lock 及其子類(lèi) ReentrantLock 的使用和注意事項(xiàng),Lock 在 Java 中占據(jù)了鎖的半壁江山,但在使用時(shí)卻要注意 4 個(gè)問(wèn)題:
- 默認(rèn)情況下 ReentrantLock 為非公平鎖而非公平鎖;
- 加鎖次數(shù)和釋放鎖次數(shù)一定要保持一致,否則會(huì)導(dǎo)致線(xiàn)程阻塞或程序異常;
- 加鎖操作一定要放在 try 代碼之前,這樣可以避免未加鎖成功又釋放鎖的異常;
- 釋放鎖一定要放在 finally 中,否則會(huì)導(dǎo)致線(xiàn)程阻塞。
到此這篇關(guān)于Java中ReentrantLock 4種常見(jiàn)的 坑的文章就介紹到這了,更多相關(guān)ReentrantLock 坑內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java高級(jí)特性之反射機(jī)制實(shí)例詳解
這篇文章主要介紹了Java高級(jí)特性之反射機(jī)制,結(jié)合實(shí)例形式詳細(xì)分析了Java反射機(jī)制原理、功能、使用方法及相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2018-08-08Java基礎(chǔ)開(kāi)發(fā)之JDBC操作數(shù)據(jù)庫(kù)增刪改查,分頁(yè)查詢(xún)實(shí)例詳解
這篇文章主要介紹了Java基礎(chǔ)開(kāi)發(fā)之JDBC操作數(shù)據(jù)庫(kù)增刪改查,分頁(yè)查詢(xún)實(shí)例詳解,需要的朋友可以參考下2020-02-02帶你了解mybatis如何實(shí)現(xiàn)讀寫(xiě)分離
本篇文章主要介紹了MyBatis實(shí)現(xiàn)數(shù)據(jù)讀寫(xiě)分離的實(shí)例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能給你帶來(lái)幫助2021-08-08Spring Boot中使用Actuator的/info端點(diǎn)輸出Git版本信息
這篇文章主要介紹了Spring Boot中使用Actuator的/info端點(diǎn)輸出Git版本信息,需要的朋友可以參考下2017-06-06Java concurrency之AtomicReference原子類(lèi)_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
AtomicReference是作用是對(duì)"對(duì)象"進(jìn)行原子操作。這篇文章主要介紹了Java concurrency之AtomicReference原子類(lèi),需要的朋友可以參考下2017-06-06Java中對(duì)于并發(fā)問(wèn)題的處理思路分享
并發(fā)粗暴的解釋就是一段代碼,在同一時(shí)間段內(nèi),被多個(gè)線(xiàn)程同時(shí)處理的情況就是并發(fā)現(xiàn)象。這篇文章和大家分享了一些對(duì)于并發(fā)問(wèn)題的處理思路,需要的可以參考一下2023-02-02Spring?Security權(quán)限管理實(shí)現(xiàn)接口動(dòng)態(tài)權(quán)限控制
這篇文章主要為大家介紹了Spring?Security權(quán)限管理實(shí)現(xiàn)接口動(dòng)態(tài)權(quán)限控制,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06SpringBoot讀取多環(huán)境配置文件的幾種方式
這篇文章主要給大家介紹了SpringBoot讀取多環(huán)境配置文件的幾種方式,文章通過(guò)代碼示例介紹的非常詳細(xì),具有一定的參考價(jià)值,需要的朋友可以參考下2023-10-10Java多線(xiàn)程的常用創(chuàng)建方式總結(jié)
今天給大家?guī)?lái)的是關(guān)于Java多線(xiàn)程的相關(guān)知識(shí),文章圍繞著Java多線(xiàn)程的常用創(chuàng)建方式展開(kāi),文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下2021-06-06