java并發(fā)編程專題(五)----詳解(JUC)ReentrantLock
上一節(jié)我們了解了Lock接口的一些簡(jiǎn)單的說(shuō)明,知道Lock鎖的常用形式,那么這節(jié)我們正式開始進(jìn)入JUC鎖(java.util.concurrent包下的鎖,簡(jiǎn)稱JUC鎖)。下面我們來(lái)看一下Lock最常用的實(shí)現(xiàn)類ReentrantLock。
1.ReentrantLock簡(jiǎn)介
由單詞意思我們可以知道這是可重入的意思。那么可重入對(duì)于鎖而言到底意味著什么呢?簡(jiǎn)單來(lái)說(shuō),它有一個(gè)與鎖相關(guān)的獲取計(jì)數(shù)器,如果擁有鎖的某個(gè)線程再次得到鎖,那么獲取計(jì)數(shù)器就加1,然后鎖需要被釋放兩次才能獲得真正釋放。這模仿了 synchronized 的語(yǔ)義;如果線程進(jìn)入由線程已經(jīng)擁有的監(jiān)控器保護(hù)的 synchronized 塊,就允許線程繼續(xù)進(jìn)行,當(dāng)線程退出第二個(gè)(或者后續(xù)) synchronized 塊的時(shí)候,不釋放鎖,只有線程退出它進(jìn)入的監(jiān)控器保護(hù)的第一個(gè) synchronized 塊時(shí),才釋放鎖。
1.1公平鎖與非公平鎖
我們查看ReentrantLock的源碼可以看到無(wú)參構(gòu)造函數(shù)是這樣的:
public ReentrantLock() {
sync = new NonfairSync();
}
NonfairSync()方法為一個(gè)非公平鎖的實(shí)現(xiàn)方法,另外Reentrantlock還有一個(gè)有參的構(gòu)造方法:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
它允許您選擇想要一個(gè) 公平(fair)鎖,還是一個(gè) 不公平(unfair)鎖。公平鎖使線程按照請(qǐng)求鎖的順序依次獲得鎖;而不公平鎖則允許直接獲取鎖,在這種情況下,線程有時(shí)可以比先請(qǐng)求鎖的其他線程先得到鎖。
為什么我們不讓所有的鎖都公平呢?畢竟,公平是好事,不公平是不好的,不是嗎?(當(dāng)孩子們想要一個(gè)決定時(shí),總會(huì)叫嚷“這不公平”。我們認(rèn)為公平非常重要,孩子們也知道。)在現(xiàn)實(shí)中,公平保證了鎖是非常健壯的鎖,有很大的性能成本。要確保公平所需要的記帳(bookkeeping)和同步,就意味著被爭(zhēng)奪的公平鎖要比不公平鎖的吞吐率更低。作為默認(rèn)設(shè)置,應(yīng)當(dāng)把公平設(shè)置為 false ,除非公平對(duì)您的算法至關(guān)重要,需要嚴(yán)格按照線程排隊(duì)的順序?qū)ζ溥M(jìn)行服務(wù)。
下面我們先來(lái)看一個(gè)例子:
public class TestReentrantLock implements Runnable{
ReentrantLock lock = new ReentrantLock();
public void get() {
lock.lock();
System.out.println(Thread.currentThread().getId());
set();
lock.unlock();
}
public void set() {
lock.lock();
System.out.println(Thread.currentThread().getId());
lock.unlock();
}
@Override
public void run() {
get();
}
public static void main(String[] args) {
TestReentrantLock ss = new TestReentrantLock();
new Thread(ss).start();
new Thread(ss).start();
new Thread(ss).start();
}
}
運(yùn)行結(jié)果:
10
10
12
12
11
11Process finished with exit code 0
由結(jié)果我們可以看出同一個(gè)線程進(jìn)入了同一個(gè)ReentrantLock鎖兩次。
2.condition條件變量
我們知道根類 Object 包含某些特殊的方法,用來(lái)在線程的 wait() 、 notify() 和 notifyAll() 之間進(jìn)行通信。那么為了在對(duì)象上 wait 或 notify ,您必須持有該對(duì)象的鎖。就像 Lock 是同步的概括一樣, Lock 框架包含了對(duì) wait 和 notify 的概括,這個(gè)概括叫作 條件(Condition)。 Condition 的方法與 wait 、 notify 和 notifyAll 方法類似,分別命名為 await 、 signal 和signalAll ,因?yàn)樗鼈儾荒芨采w Object 上的對(duì)應(yīng)方法。
首先我們來(lái)計(jì)算一道題:
我們要打印1到9這9個(gè)數(shù)字,由A線程先打印1,2,3,然后由B線程打印4,5,6,然后再由A線程打印7,8,9. 這道題有很多種解法,我們先用Object的wait,notify方法來(lái)實(shí)現(xiàn):
public class WaitNotifyDemo {
private volatile int val = 1;
private synchronized void printAndIncrease() {
System.out.println(Thread.currentThread().getName() +"prints " + val);
val++;
}
// print 1,2,3 7,8,9
public class PrinterA implements Runnable {
@Override
public void run() {
while (val <= 3) {
printAndIncrease();
}
// print 1,2,3 then notify printerB
synchronized (WaitNotifyDemo.this) {
System.out.println("PrinterA printed 1,2,3; notify PrinterB");
WaitNotifyDemo.this.notify();
}
try {
while (val <= 6) {
synchronized (WaitNotifyDemo.this) {
System.out.println("wait in printerA");
WaitNotifyDemo.this.wait();
}
}
System.out.println("wait end printerA");
} catch (InterruptedException e) {
e.printStackTrace();
}
while (val <= 9) {
printAndIncrease();
}
System.out.println("PrinterA exits");
}
}
// print 4,5,6 after printA print 1,2,3
public class PrinterB implements Runnable {
@Override
public void run() {
while (val < 3) {
synchronized (WaitNotifyDemo.this) {
try {
System.out
.println("printerB wait for printerA printed 1,2,3");
WaitNotifyDemo.this.wait();
System.out
.println("printerB waited for printerA printed 1,2,3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
while (val <= 6) {
printAndIncrease();
}
System.out.println("notify in printerB");
synchronized (WaitNotifyDemo.this) {
WaitNotifyDemo.this.notify();
}
System.out.println("notify end printerB");
System.out.println("PrinterB exits.");
}
}
public static void main(String[] args) {
WaitNotifyDemo demo = new WaitNotifyDemo();
demo.doPrint();
}
private void doPrint() {
PrinterA pa = new PrinterA();
PrinterB pb = new PrinterB();
Thread a = new Thread(pa);
a.setName("printerA");
Thread b = new Thread(pb);
b.setName("printerB");
// 必須讓b線程先執(zhí)行,否則b線程有可能得不到鎖,執(zhí)行不了wait,而a線程一直持有鎖,會(huì)先notify了
b.start();
a.start();
}
}
運(yùn)行結(jié)果為:
printerB wait for printerA printed 1,2,3
printerA prints 1
printerA prints 2
printerA prints 3
PrinterA printed 1,2,3; notify PrinterB
wait in printerA
printerB waited for printerA printed 1,2,3
printerB prints 4
printerB prints 5
printerB prints 6
notify in printerB
notify end printerB
wait end printerA
printerA prints 7
printerA prints 8
printerA prints 9
PrinterA exits
PrinterB exits.Process finished with exit code 0
我們來(lái)分析一下上面的程序:
首先在main方法中我們看到是先啟動(dòng)了B線程,因?yàn)锽線程持有wait()對(duì)象,而A線程則持有notify(),如果先啟動(dòng)A有可能會(huì)造成死鎖的狀態(tài)。
B線程啟動(dòng)以后進(jìn)入run()方法:
while (val < 3) {
synchronized (WaitNotifyDemo.this) {
try {
System.out.println("printerB wait for printerA printed 1,2,3");
WaitNotifyDemo.this.wait();
System.out.println("printerB waited for printerA printed 1,2,3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
while (val <= 6) {
printAndIncrease();
}
這里有一個(gè)while循環(huán),如果val的值小于3,那么在WaitNotifyDemo的實(shí)例的同步塊中調(diào)用WaitNotifyDemo.this.wait()方法,這里要注意無(wú)論是wait,還是notify,notifyAll方法都需要在其實(shí)例對(duì)象的同步塊中執(zhí)行,這樣當(dāng)前線程才能獲得同步實(shí)例的同步控制權(quán),如果不在同步塊中執(zhí)行wait或者notify方法會(huì)出java.lang.IllegalMonitorStateException異常。另外還要注意在wait方法兩邊的同步塊會(huì)在wait執(zhí)行完畢之后釋放對(duì)象鎖。
這樣PrinterB就進(jìn)入了等待狀態(tài),我們?cè)倏聪翽rinterA的run方法:
while (val <= 3) {
printAndIncrease();
}
// print 1,2,3 then notify printerB
synchronized (WaitNotifyDemo.this) {
System.out.println("PrinterA printed 1,2,3; notify PrinterB");
WaitNotifyDemo.this.notify();
}
try {
while (val <= 6) {
synchronized (WaitNotifyDemo.this) {
System.out.println("wait in printerA");
WaitNotifyDemo.this.wait();
}
}
System.out.println("wait end printerA");
} catch (InterruptedException e) {
e.printStackTrace();
}
這里首先打印了1、2、3,然后在同步塊中調(diào)用了WaitNotifyDemo實(shí)例的notify方法,這樣PrinterB就得到了繼續(xù)執(zhí)行的通知,然后PrinterA進(jìn)入等待狀態(tài),等待PrinterB通知。
我們?cè)倏聪翽rinterB run方法剩下的代碼:
while (val <= 6) {
printAndIncrease();
}
System.out.println("notify in printerB");
synchronized (WaitNotifyDemo.this) {
WaitNotifyDemo.this.notify();
}
System.out.println("notify end printerB");
System.out.println("PrinterB exits.");
PrinterB首先打印了4、5、6,然后在同步塊中調(diào)用了notify方法,通知PrinterA開始執(zhí)行。
PrinterA得到通知后,停止等待,打印剩下的7、8、9三個(gè)數(shù)字,如下是PrinterA run方法中剩下的代碼:
while (val <= 9) {
printAndIncrease();
}
整個(gè)程序就分析完了,下面我們?cè)賮?lái)使用Condition來(lái)做這道題:
public class TestCondition {
static class NumberWrapper {
public int value = 1;
}
public static void main(String[] args) {
//初始化可重入鎖
final Lock lock = new ReentrantLock();
//第一個(gè)條件當(dāng)屏幕上輸出到3
final Condition reachThreeCondition = lock.newCondition();
//第二個(gè)條件當(dāng)屏幕上輸出到6
final Condition reachSixCondition = lock.newCondition();
//NumberWrapper只是為了封裝一個(gè)數(shù)字,一邊可以將數(shù)字對(duì)象共享,并可以設(shè)置為final
//注意這里不要用Integer, Integer 是不可變對(duì)象
final NumberWrapper num = new NumberWrapper();
//初始化A線程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
//需要先獲得鎖
lock.lock();
try {
System.out.println("threadA start write");
//A線程先輸出前3個(gè)數(shù)
while (num.value <= 3) {
System.out.println(num.value);
num.value++;
}
//輸出到3時(shí)要signal,告訴B線程可以開始了
reachThreeCondition.signal();
} finally {
lock.unlock();
}
lock.lock();
try {
//等待輸出6的條件
reachSixCondition.await();
System.out.println("threadA start write");
//輸出剩余數(shù)字
while (num.value <= 9) {
System.out.println(num.value);
num.value++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
while (num.value <= 3) {
//等待3輸出完畢的信號(hào)
reachThreeCondition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
try {
lock.lock();
//已經(jīng)收到信號(hào),開始輸出4,5,6
System.out.println("threadB start write");
while (num.value <= 6) {
System.out.println(num.value);
num.value++;
}
//4,5,6輸出完畢,告訴A線程6輸出完了
reachSixCondition.signal();
} finally {
lock.unlock();
}
}
});
//啟動(dòng)兩個(gè)線程
threadB.start();
threadA.start();
}
}
基本思路就是首先要A線程先寫1,2,3,這時(shí)候B線程應(yīng)該等待reachThredCondition信號(hào),而當(dāng)A線程寫完3之后就通過(guò)signal告訴B線程“我寫到3了,該你了”,這時(shí)候A線程要等嗲reachSixCondition信號(hào),同時(shí)B線程得到通知,開始寫4,5,6,寫完4,5,6之后B線程通知A線程reachSixCondition條件成立了,這時(shí)候A線程就開始寫剩下的7,8,9了。
我們可以看到上例中我們創(chuàng)建了兩個(gè)Condition,在不同的情況下可以使用不同的Condition,與wait和notify相比提供了更細(xì)致的控制。
3.線程阻塞原語(yǔ)–LockSupport
我們一再提線程、鎖等概念,但鎖是如果實(shí)現(xiàn)的呢?又是如何知道當(dāng)前阻塞線程的又是哪個(gè)對(duì)象呢?LockSupport是JDK中比較底層的類,用來(lái)創(chuàng)建鎖和其他同步工具類的基本線程阻塞原語(yǔ)。
java鎖和同步器框架的核心 AQS: AbstractQueuedSynchronizer,就是通過(guò)調(diào)用 LockSupport .park()和 LockSupport .unpark()實(shí)現(xiàn)線程的阻塞和喚醒 的。 LockSupport 很類似于二元信號(hào)量(只有1個(gè)許可證可供使用),如果這個(gè)許可還沒(méi)有被占用,當(dāng)前線程獲取許可并繼 續(xù) 執(zhí)行;如果許可已經(jīng)被占用,當(dāng)前線 程阻塞,等待獲取許可。
LockSupport是針對(duì)特定線程來(lái)進(jìn)行阻塞和解除阻塞操作的;而Object的wait()/notify()/notifyAll()是用來(lái)操作特定對(duì)象的等待集合的。
LockSupport的兩個(gè)主要方法是park()和Unpark(),我們來(lái)看一下他們的實(shí)現(xiàn):
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
unsafe.park(false, 0L);
setBlocker(t, null);
}
public static void park() {
unsafe.park(false, 0L);
}
public static void unpark(Thread thread) {
if (thread != null)
unsafe.unpark(thread);
}
由源碼我們可見(jiàn)在park方法內(nèi)部首先獲得當(dāng)前線程然后阻塞當(dāng)前線程,unpark方法傳入一個(gè)可配置的線程來(lái)為該線程解鎖。以“線程”作為方法的參數(shù), 語(yǔ)義更清晰,使用起來(lái)也更方便。而wait/notify的實(shí)現(xiàn)使得“線程”的阻塞/喚醒對(duì)線程本身來(lái)說(shuō)是被動(dòng)的,要準(zhǔn)確的控制哪個(gè)線程、什么時(shí)候阻塞/喚醒很困難, 要不隨機(jī)喚醒一個(gè)線程(notify)要不喚醒所有的(notifyAll)。
下面我們來(lái)看一個(gè)例子:
public class TestLockSupport {
public static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
static ChangeObjectThread t2 = new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name) {
super.setName(name);
}
public void run() {
synchronized (u) {
System.out.println("in" + getName());
LockSupport.park();
}
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(2000);
t2.start();
LockSupport.unpark(t1);
LockSupport.unpark(t2);
t1.join();
t2.join();
}
}
當(dāng)我們把”LockSupport.unpark(t1);”這一句注掉的話我們會(huì)發(fā)現(xiàn)程序陷入死鎖。而且我們看到再main方法中unpark是在t1和t2啟動(dòng)之后才執(zhí)行,但是為什么t1啟動(dòng)之后,t2也啟動(dòng)了呢?注意,**unpark函數(shù)可以先于park調(diào)用。比如線程B調(diào)用unpark函數(shù),給線程A發(fā)了一個(gè)“許可”,那么當(dāng)線程A調(diào)用park時(shí),它發(fā)現(xiàn)已經(jīng)有“許可”了,那么它會(huì)馬上再繼續(xù)運(yùn)行。**unpark函數(shù)為線程提供“許可(permit)”,線程調(diào)用park函數(shù)則等待“許可”。這個(gè)有點(diǎn)像信號(hào)量,但是這個(gè)“許可”是不能疊加的,“許可”是一次性的。比如線程B連續(xù)調(diào)用了三次unpark函數(shù),當(dāng)線程A調(diào)用park函數(shù)就使用掉這個(gè)“許可”,如果線程A再次調(diào)用park,則進(jìn)入等待狀態(tài)。
除了有定時(shí)阻塞的功能外,還支持中斷影響,但是和其他接收中斷函數(shù)不一樣,他不會(huì)拋出
InterruptedException異常,他只會(huì)默默的返回,但是我們可以從Thread.Interrupted()等方法獲得中斷標(biāo)記.
我們來(lái)看一個(gè)例子:
public class TestLockSupport {
public static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
static ChangeObjectThread t2 = new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name) {
super.setName(name);
}
public void run() {
synchronized (u) {
System.out.println("in " + getName());
LockSupport.park();
if (Thread.interrupted()) {
System.out.println(getName() + " 被中斷了!");
}
}
System.out.println(getName() + " 執(zhí)行結(jié)束");
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(100);
t2.start();
t1.interrupt();
LockSupport.unpark(t2);
}
}
輸出:
in t1
t1 被中斷了!
t1 執(zhí)行結(jié)束
in t2
t2 執(zhí)行結(jié)束Process finished with exit code 0
由run方法中的終端異常捕獲我們可以看到線程在中斷時(shí)并沒(méi)有拋出異常而是正常執(zhí)行下去了。
關(guān)于LockSupport其實(shí)要介紹的東西還是很多,因?yàn)檫@個(gè)類實(shí)現(xiàn)了底層的一些方法,各種的鎖實(shí)現(xiàn)都是這個(gè)基礎(chǔ)上發(fā)展而來(lái)的。以后會(huì)專門用一個(gè)篇章來(lái)學(xué)習(xí)jdk內(nèi)部的阻塞機(jī)制。說(shuō)前面我們講到Object的wait和notify,講到Condition條件,講到j(luò)dk中不對(duì)外部暴露的LockSupport阻塞原語(yǔ),那么在JUC包中還有另外一個(gè)阻塞機(jī)制—信號(hào)量機(jī)制(Semaphore),下一節(jié)我們一起探討一下。
以上就是java并發(fā)編程專題(五)----詳解(JUC)ReentrantLock的詳細(xì)內(nèi)容,更多關(guān)于java ReentrantLock的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java利用FileUtils讀取數(shù)據(jù)和寫入數(shù)據(jù)到文件
這篇文章主要介紹了Java利用FileUtils讀取數(shù)據(jù)和寫入數(shù)據(jù)到文件,下面文章圍繞FileUtils的相關(guān)資料展開怎么讀取數(shù)據(jù)和寫入數(shù)據(jù)到文件的內(nèi)容,具有一定的參考價(jià)值,徐婭奧德小伙伴可以參考一下2021-12-12
Springboot的自動(dòng)配置是什么及注意事項(xiàng)
SpringBoot的自動(dòng)配置(Auto-configuration)是指框架根據(jù)項(xiàng)目的依賴和應(yīng)用程序的環(huán)境自動(dòng)配置Spring應(yīng)用上下文中的Bean和組件,目的是簡(jiǎn)化開發(fā)者的配置工作,本文介紹Springboot的自動(dòng)配置是什么及注意事項(xiàng),感興趣的朋友一起看看吧2025-03-03
SpringBoot如何實(shí)現(xiàn)緩存預(yù)熱
緩存預(yù)熱是指在 Spring Boot 項(xiàng)目啟動(dòng)時(shí),預(yù)先將數(shù)據(jù)加載到緩存系統(tǒng)(如 Redis)中的一種機(jī)制,本文主要介紹了SpringBoot如何實(shí)現(xiàn)緩存預(yù)熱,感興趣的可以了解下2024-12-12
Java 異常的棧軌跡(Stack Trace)詳解及實(shí)例代碼
這篇文章主要介紹了Java 異常的棧軌跡(Stack Trace)詳解及實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-03-03
IDEA遇到Internal error. Please refer to http://jb. gg/ide/crit
這篇文章主要介紹了IDEA遇到Internal error. Please refer to http://jb. gg/ide/critical-startup-errors的問(wèn)題及解決辦法,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),需要的朋友可以參考下2020-08-08

