淺談Java并發(fā)中ReentrantLock鎖應(yīng)該怎么用
重入鎖可以替代關(guān)鍵字 synchronized 。
在 JDK5.0 的早期版本中,重入鎖的性能遠(yuǎn)遠(yuǎn)優(yōu)于關(guān)鍵字 synchronized ,
但從 JDK6.0 開始, JDK 在關(guān)鍵字 synchronized 上做了大量的優(yōu)化,使得兩者的性能差距并不大。
重入鎖使用 ReentrantLock 實(shí)現(xiàn)
1、重入鎖
package com.shockang.study.java.concurrent.lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
lock.lock();
lock.lock();
try {
i++;
} finally {
lock.unlock();
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo tl = new ReentrantLockDemo();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
控制臺(tái)打印
20000000
說(shuō)明
一個(gè)線程連續(xù)兩次獲得同一把鎖是允許的。
如果不允許這么操作,那么同一個(gè)線程在第 2 次獲得鎖時(shí),將會(huì)和自己產(chǎn)生死鎖。
程序就會(huì)“卡死”在第 2 次申請(qǐng)鎖的過(guò)程中。
但需要注意的是,如果同一個(gè)線程多次獲得鎖,那么在釋放鎖的時(shí)候,也必須釋放相同次數(shù)。
如果釋放鎖的次數(shù)多了,那么會(huì)得到一個(gè) java.lang.IllegalMonitorStateException 異常,反之,如果釋放鎖的次數(shù)少了,那么相當(dāng)于線程還持有這個(gè)鎖,因此,其他線程也無(wú)法進(jìn)入臨界區(qū)。
2、中斷響應(yīng)
對(duì)于關(guān)鍵字 synchronized 來(lái)說(shuō),如果一個(gè)線程在等待鎖,那么結(jié)果只有兩種情況,要么它獲得這把鎖繼續(xù)執(zhí)行,要么它就保持等待。
而使用重入鎖,則提供另外一種可能,那就是線程可以被中斷。
也就是在等待鎖的過(guò)程中,程序可以根據(jù)需要取消對(duì)鎖的請(qǐng)求。
有些時(shí)候,這么做是非常有必要的。
比如,你和朋友約好一起去打球,如果你等了半個(gè)小時(shí)朋友還沒(méi)有到,你突然接到一個(gè)電話,說(shuō)由于突發(fā)情況,朋友不能如約前來(lái)了,那么你一定掃興地打道回府了。
中斷正是提供了一套類似的機(jī)制。
如果一個(gè)線程正在等待鎖,那么它依然可以收到一個(gè)通知,被告知無(wú)須等待,可以停止工作了。
這種情況對(duì)于處理死鎖是有一定幫助的。
下面的代碼產(chǎn)生了一個(gè)死鎖,但得益于鎖中斷,我們可以很輕易地解決這個(gè)死鎖。
package com.shockang.study.java.concurrent.lock;
import java.util.concurrent.locks.ReentrantLock;
public class IntLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
/**
* 控制加鎖順序,方便構(gòu)造死鎖
*
* @param lock
*/
public IntLock(int lock) {
this.lock = lock;
}
@Override
public void run() {
try {
if (lock == 1) {
lock1.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread())
lock1.unlock();
if (lock2.isHeldByCurrentThread())
lock2.unlock();
System.out.println(Thread.currentThread().getId() + ":線程退出");
}
}
public static void main(String[] args) throws InterruptedException {
IntLock r1 = new IntLock(1);
IntLock r2 = new IntLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
Thread.sleep(1000);
//中斷其中一個(gè)線程
t2.interrupt();
}
}
控制臺(tái)輸出
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.shockang.study.java.concurrent.lock.IntLock.run(IntLock.java:35)
at java.lang.Thread.run(Thread.java:748)
11:線程退出
12:線程退出
說(shuō)明
線程 t1 和 t2 啟動(dòng)后, t1 先占用 lock1 ,再占用 lock2。
t2 先占用 lock2 ,再請(qǐng)求 lock1。
因此,很容易形成 t1 和 t2 之間的相互等待。
在這里,對(duì)鎖的請(qǐng)求,統(tǒng)一使用 lockInterruptibly() 方法。
這是一個(gè)可以對(duì)中斷進(jìn)行響應(yīng)的鎖申請(qǐng)動(dòng)作,即在等待鎖的過(guò)程中,可以響應(yīng)中斷。
在代碼第 56 行,主線程 main 處于休眠狀態(tài),此時(shí),這兩個(gè)線程處于死鎖的狀態(tài)。
在代碼第 58 行,由于 t2 線程被中斷,故 t2 會(huì)放棄對(duì) lock1 的申請(qǐng),同時(shí)釋放已獲得的 lock2 。
這個(gè)操作導(dǎo)致 t1 線程可以順利得到 lock2 而繼續(xù)執(zhí)行下去。
3、鎖申請(qǐng)等待限時(shí)
除了等待外部通知之外,要避免死鎖還有另外一種方法,那就是限時(shí)等待。
依然以約朋友打球?yàn)槔?,如果朋友退退不?lái),又無(wú)法聯(lián)系到他,那么在等待 1 到 2 個(gè)小時(shí)后,我想大部分人都會(huì)掃興離去。
對(duì)線程來(lái)說(shuō)也是這樣。
通常,我們無(wú)法判斷為什么一個(gè)線程退遲拿不到鎖。
也許是因?yàn)樗梨i了,也許是因?yàn)楫a(chǎn)生了饑餓。
如果給定一個(gè)等待時(shí)間,讓線程自動(dòng)放棄,那么對(duì)系統(tǒng)來(lái)說(shuō)是有意義的。
我們可以使用 tryLock() 方法進(jìn)行一次限時(shí)的等待。
tryLock(long, TimeUnit)
下面這段代碼展示了限時(shí)等待鎖的使用。
package com.shockang.study.java.concurrent.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TimeLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
Thread.sleep(6000);
} else {
System.out.println("get lock failed");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
TimeLock tl = new TimeLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
}
}
控制臺(tái)打印
get lock failed
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
at com.shockang.study.java.concurrent.lock.TimeLock.run(TimeLock.java:20)
at java.lang.Thread.run(Thread.java:748)
說(shuō)明
在這里, tryLock() 方法接收兩個(gè)參數(shù),一個(gè)表示等待時(shí)長(zhǎng),另外一個(gè)表示計(jì)時(shí)單位。
這里的單位設(shè)置為秒,時(shí)長(zhǎng)為 5 ,表示線程在這個(gè)鎖請(qǐng)求中最多等待 5 秒。
如果超過(guò) 5 秒還沒(méi)有得到鎖,就會(huì)返回 false 。
如果成功獲得鎖,則返回 true 。
在本例中,由于占用鎖的線程會(huì)持有鎖長(zhǎng)達(dá) 6 秒,故另一個(gè)線程無(wú)法在 5 秒的等待時(shí)間內(nèi)獲得鎖,因此請(qǐng)求鎖會(huì)失敗。
tryLock()
ReentrantLock.tryLock() 方法也可以不帶參數(shù)直接運(yùn)行。
在這種情況下,當(dāng)前線程會(huì)嘗試獲得鎖,如果鎖并未被其他線程占用,則申請(qǐng)鎖會(huì)成功,并立即返回 true 。
如果鎖被其他線程占用,則當(dāng)前線程不會(huì)進(jìn)行等待,而是立即返回 false 。
這種模式不會(huì)引起線程等待,因此也不會(huì)產(chǎn)生死鎖。
package com.shockang.study.java.concurrent.lock;
import java.util.concurrent.locks.ReentrantLock;
public class TryLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public TryLock(int lock) {
this.lock = lock;
}
@Override
public void run() {
if (lock == 1) {
while (true) {
if (lock1.tryLock()) {
try {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
if (lock2.tryLock()) {
try {
System.out.println(Thread.currentThread()
.getId() + ":My Job done");
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
}
} else {
while (true) {
if (lock2.tryLock()) {
try {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
if (lock1.tryLock()) {
try {
System.out.println(Thread.currentThread()
.getId() + ":My Job done");
return;
} finally {
lock1.unlock();
}
}
} finally {
lock2.unlock();
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
TryLock r1 = new TryLock(1);
TryLock r2 = new TryLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
控制臺(tái)輸出
11:My Job done
12:My Job done
說(shuō)明
上述代碼采用了非常容易死鎖的加鎖順序。
也就是先讓 t1 獲得 lock1 ,再讓 2 獲得 lock2 ,接著做反向請(qǐng)求,讓 t1 申請(qǐng) lock2 , t2 申請(qǐng) lock1 。
在一般情況下,這會(huì)導(dǎo)致 t1 和 2 相互等待。
待,從而引起死鎖。
但是使用 tryLock() 方法后,這種情況就大大改善了。
由于線程不會(huì)傻傻地等待,而是不停地嘗試,因此,只要執(zhí)行足夠長(zhǎng)的時(shí)間,線程總是會(huì)得到所有需要的資源,從而正常執(zhí)行(這里以線程同時(shí)獲得 lock1 和 lock2 兩把鎖,作為其可以正常執(zhí)行的條件)。
在同時(shí)獲得 lock1 和 lock2 后,線程就打印出標(biāo)志著任務(wù)完成的信息“ My Job done”。
4、公平鎖
在大多數(shù)情況下,鎖的申請(qǐng)都是非公平的。
也就是說(shuō),線程 1 首先請(qǐng)求了鎖 A ,接著線程 2 也請(qǐng)求了鎖 A 。
那么當(dāng)鎖 A 可用時(shí),是線程 1 可以獲得鎖還是線程 2 可以獲得鎖呢?
這是不一定的,系統(tǒng)只是會(huì)從這個(gè)鎖的等待隊(duì)列中隨機(jī)挑選一個(gè)。
因此不能保證其公平性。
這就好比買票不排隊(duì),大家都圍在售票窗口前,售票員忙得焦頭爛額,也顧不及誰(shuí)先誰(shuí)后,隨便找個(gè)人出票就完事了。
而公平的鎖,則不是這樣,它會(huì)按照時(shí)間的先后順序,保證先到者先得,后到者后得。
公平鎖的一大特點(diǎn)是:它不會(huì)產(chǎn)生饑餓現(xiàn)象。
關(guān)于線程饑餓請(qǐng)參考我的博客——死鎖、活鎖和饑餓是什么意思?
只要你排隊(duì),最終還是可以等到資源的。
如果我們使用 synchronized 關(guān)鍵字進(jìn)行鎖控制,那么產(chǎn)生的鎖就是非公平的。
而重入鎖允許我們對(duì)其公平性進(jìn)行設(shè)置。
它的構(gòu)造函數(shù)如下:
/**
* 使用給定的公平策略創(chuàng)建一個(gè) ReentrantLock 的實(shí)例。
*
* @param fair 如果此鎖應(yīng)使用公平排序策略為 true
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
當(dāng)參數(shù) fair 為 true 時(shí),表示鎖是公平的。
公平鎖看起來(lái)很優(yōu)美,但是要實(shí)現(xiàn)公平鎖必然要求系統(tǒng)維護(hù)一個(gè)有序隊(duì)列,因此公平鎖的實(shí)現(xiàn)成本比較高,性能卻非常低下,因此,在默認(rèn)情況下,鎖是非公平的。
如果沒(méi)有特別的需求,則不需要使用公平鎖。
公平鎖和非公平鎖在線程調(diào)度表現(xiàn)上也是非常不一樣的。
下面的代碼可以很好地突出公平鎖的特點(diǎn)。
package com.shockang.study.java.concurrent.lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairLock implements Runnable {
public static ReentrantLock fairLock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
try {
fairLock.lock();
System.out.println(Thread.currentThread().getName() + " 獲得鎖");
} finally {
fairLock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
FairLock r1 = new FairLock();
Thread t1 = new Thread(r1, "Thread_t1");
Thread t2 = new Thread(r1, "Thread_t2");
t1.start();
t2.start();
}
}
控制臺(tái)輸出
獲得鎖
Thread_t2 獲得鎖
Thread_t2 獲得鎖
Thread_t2 獲得鎖
Thread_t2 獲得鎖
Thread_t1 獲得鎖
Thread_t1 獲得鎖
Thread_t2 獲得鎖
Thread_t2 獲得鎖
Thread_t2 獲得鎖
Thread_t1 獲得鎖
Thread_t1 獲得鎖
# 省略
說(shuō)明
由于代碼會(huì)產(chǎn)生大量輸出,這里只截取部分進(jìn)行說(shuō)明。
在這個(gè)輸出中,很明顯可以看到,兩個(gè)線程基本上是交替獲得鎖的,幾乎不會(huì)發(fā)生同一個(gè)線程連續(xù)多次獲得鎖的可能,從而保證了公平性。
如果設(shè)置了 false,則會(huì)根據(jù)系統(tǒng)的調(diào)度,一個(gè)線程會(huì)傾向于再次獲取已經(jīng)持有的鎖,這種分配方式是高效的,但是無(wú)公平性可言。
源碼(JDK8)
/**
* 一種可重入互斥鎖,其基本行為和語(yǔ)義與使用同步方法和語(yǔ)句訪問(wèn)的隱式監(jiān)視鎖(即 synchronized)相同,但具有擴(kuò)展功能。
*
* 可重入鎖屬于上次成功鎖定但尚未解鎖它的線程。
*
* 當(dāng)鎖不屬于另一個(gè)線程時(shí),調(diào)用鎖的線程將返回,并成功獲取鎖。
*
* 如果當(dāng)前線程已經(jīng)擁有鎖,則該方法將立即返回。這可以使用 isHeldByCurrentThread 和 getHoldCount 方法進(jìn)行檢查。
*
* 此類的構(gòu)造函數(shù)接受可選的公平性參數(shù)。
*
* 當(dāng)設(shè)置為 true 時(shí),在競(jìng)爭(zhēng)狀態(tài)下,鎖有利于向等待時(shí)間最長(zhǎng)的線程授予訪問(wèn)權(quán)限。否則,此鎖不保證任何特定的訪問(wèn)順序。
*
* 使用由多線程訪問(wèn)的公平鎖的程序可能顯示較低的總吞吐量
*
* (即,較慢;通常比使用默認(rèn)設(shè)置的要慢得多,但是在獲得鎖和保證不饑餓的時(shí)間上有較小的差異。
*
* 但是請(qǐng)注意,鎖的公平性并不能保證線程調(diào)度的公平性。
*
* 因此,使用公平鎖的多個(gè)線程中的一個(gè)線程可以連續(xù)多次獲得公平鎖,而其他活動(dòng)線程則沒(méi)有進(jìn)行并且當(dāng)前沒(méi)有持有該鎖。
*
* 還要注意,untimed tryLock() 方法不支持公平性設(shè)置。
*
* 如果鎖可用,即使其他線程正在等待,它也會(huì)成功。
*
* 建議的做法是總是在調(diào)用之后立即使用try塊鎖定,最典型的是在構(gòu)建之前/之后,例如:
*
* class X {
* private final ReentrantLock lock = new ReentrantLock();
* // ...
*
* public void m() {
* lock.lock(); // block until condition holds
* try {
* // ... method body
* } finally {
* lock.unlock()
* }
* }
* }}
*
* 除了實(shí)現(xiàn)鎖接口之外,這個(gè)類還定義了許多公共和受保護(hù)的方法來(lái)檢查鎖的狀態(tài)。
*
* 其中一些方法只對(duì) instrumentation 和 monitoring 有用。
*
* 此類的序列化與內(nèi)置鎖的行為相同:反序列化的鎖處于未鎖定狀態(tài),而與序列化時(shí)的狀態(tài)無(wú)關(guān)。
*
* 此鎖最多支持同一線程的2147483647個(gè)遞歸鎖。嘗試超過(guò)此限制會(huì)導(dǎo)致鎖定方法拋出錯(cuò)誤。
*
* @since 1.5
* @author Doug Lea
*/
public class ReentrantLock implements Lock, java.io.Serializable
到此這篇關(guān)于淺談Java并發(fā)中ReentrantLock鎖應(yīng)該怎么用的文章就介紹到這了,更多相關(guān)ReentrantLock鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
idea使用pagehelper實(shí)現(xiàn)后端分頁(yè)功能的步驟詳解
這篇文章主要介紹了idea使用pagehelper實(shí)現(xiàn)后端分頁(yè)功能的步驟,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09
Java常用鎖synchronized和ReentrantLock的區(qū)別
這篇文章主要介紹了Java常用鎖synchronized和ReentrantLock的區(qū)別,二者的功效都是相同的,但又有很多不同點(diǎn),下面我們就進(jìn)入文章了解具體的相關(guān)內(nèi)容吧。需要的小伙伴也可以參考一下2022-05-05
Spring+SpringMVC+JDBC實(shí)現(xiàn)登錄的示例(附源碼)
這篇文章主要介紹了Spring+SpringMVC+JDBC實(shí)現(xiàn)登錄的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
java監(jiān)聽器的實(shí)現(xiàn)和原理詳解
這篇文章主要給大家介紹了關(guān)于java監(jiān)聽器實(shí)現(xiàn)和原理的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
Spring Security如何在Servlet中執(zhí)行
這篇文章主要介紹了Spring Security如何在Servlet中執(zhí)行,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04
Mybatis中的resultType和resultMap查詢操作實(shí)例詳解
resultType是直接表示返回類型的,而resultMap則是對(duì)外部ResultMap的引用,resultMap解決復(fù)雜查詢是的映射問(wèn)題。這篇文章主要介紹了Mybatis中的resultType和resultMap查詢操作實(shí)例詳解,需要的朋友可以參考下2016-09-09
Java實(shí)現(xiàn)LeetCode(組合總和)
這篇文章主要介紹了Java實(shí)現(xiàn)LeetCode(組合總數(shù)),本文通過(guò)使用java實(shí)現(xiàn)leetcode的組合總數(shù)題目和實(shí)現(xiàn)思路分析,需要的朋友可以參考下2021-06-06
MyEclipse整合ssh三大框架環(huán)境搭載用戶注冊(cè)源碼下載
這篇文章主要為大家詳細(xì)介紹了如何使用MyEclipse整合ssh三大框架進(jìn)行環(huán)境搭載,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10

