java并發(fā)編程專題(五)----詳解(JUC)ReentrantLock
上一節(jié)我們了解了Lock接口的一些簡單的說明,知道Lock鎖的常用形式,那么這節(jié)我們正式開始進(jìn)入JUC鎖(java.util.concurrent包下的鎖,簡稱JUC鎖)。下面我們來看一下Lock最常用的實(shí)現(xiàn)類ReentrantLock。
1.ReentrantLock簡介
由單詞意思我們可以知道這是可重入的意思。那么可重入對(duì)于鎖而言到底意味著什么呢?簡單來說,它有一個(gè)與鎖相關(guān)的獲取計(jì)數(shù)器,如果擁有鎖的某個(gè)線程再次得到鎖,那么獲取計(jì)數(shù)器就加1,然后鎖需要被釋放兩次才能獲得真正釋放。這模仿了 synchronized 的語義;如果線程進(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的源碼可以看到無參構(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)和同步,就意味著被爭奪的公平鎖要比不公平鎖的吞吐率更低。作為默認(rèn)設(shè)置,應(yīng)當(dāng)把公平設(shè)置為 false ,除非公平對(duì)您的算法至關(guān)重要,需要嚴(yán)格按照線程排隊(duì)的順序?qū)ζ溥M(jìn)行服務(wù)。
下面我們先來看一個(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 包含某些特殊的方法,用來在線程的 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)方法。
首先我們來計(jì)算一道題:
我們要打印1到9這9個(gè)數(shù)字,由A線程先打印1,2,3,然后由B線程打印4,5,6,然后再由A線程打印7,8,9. 這道題有很多種解法,我們先用Object的wait,notify方法來實(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
我們來分析一下上面的程序:
首先在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()方法,這里要注意無論是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è)賮硎褂肅ondition來做這道題:
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之后就通過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.線程阻塞原語–LockSupport
我們一再提線程、鎖等概念,但鎖是如果實(shí)現(xiàn)的呢?又是如何知道當(dāng)前阻塞線程的又是哪個(gè)對(duì)象呢?LockSupport是JDK中比較底層的類,用來創(chuàng)建鎖和其他同步工具類的基本線程阻塞原語。
java鎖和同步器框架的核心 AQS: AbstractQueuedSynchronizer,就是通過調(diào)用 LockSupport .park()和 LockSupport .unpark()實(shí)現(xiàn)線程的阻塞和喚醒 的。 LockSupport 很類似于二元信號(hào)量(只有1個(gè)許可證可供使用),如果這個(gè)許可還沒有被占用,當(dāng)前線程獲取許可并繼 續(xù) 執(zhí)行;如果許可已經(jīng)被占用,當(dāng)前線 程阻塞,等待獲取許可。
LockSupport是針對(duì)特定線程來進(jìn)行阻塞和解除阻塞操作的;而Object的wait()/notify()/notifyAll()是用來操作特定對(duì)象的等待集合的。
LockSupport的兩個(gè)主要方法是park()和Unpark(),我們來看一下他們的實(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); }
由源碼我們可見在park方法內(nèi)部首先獲得當(dāng)前線程然后阻塞當(dāng)前線程,unpark方法傳入一個(gè)可配置的線程來為該線程解鎖。以“線程”作為方法的參數(shù), 語義更清晰,使用起來也更方便。而wait/notify的實(shí)現(xiàn)使得“線程”的阻塞/喚醒對(duì)線程本身來說是被動(dòng)的,要準(zhǔn)確的控制哪個(gè)線程、什么時(shí)候阻塞/喚醒很困難, 要不隨機(jī)喚醒一個(gè)線程(notify)要不喚醒所有的(notifyAll)。
下面我們來看一個(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)記.
我們來看一個(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í)并沒有拋出異常而是正常執(zhí)行下去了。
關(guān)于LockSupport其實(shí)要介紹的東西還是很多,因?yàn)檫@個(gè)類實(shí)現(xiàn)了底層的一些方法,各種的鎖實(shí)現(xiàn)都是這個(gè)基礎(chǔ)上發(fā)展而來的。以后會(huì)專門用一個(gè)篇章來學(xué)習(xí)jdk內(nèi)部的阻塞機(jī)制。說前面我們講到Object的wait和notify,講到Condition條件,講到j(luò)dk中不對(duì)外部暴露的LockSupport阻塞原語,那么在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面試為何阿里強(qiáng)制要求不在foreach里執(zhí)行刪除操作
那天,小二去阿里面試,面試官老王一上來就甩給了他一道面試題:為什么阿里的 Java 開發(fā)手冊(cè)里會(huì)強(qiáng)制不要在 foreach 里進(jìn)行元素的刪除操作2021-11-11Spring Boot 編寫Servlet、Filter、Listener、Interceptor的方法
這篇文章給大家介紹了spring-boot中如何定義過濾器、監(jiān)聽器和攔截器,對(duì)Spring Boot 編寫Servlet、Filter、Listener、Interceptor的相關(guān)知識(shí)感興趣的朋友一起看看吧2017-07-07java微信公眾號(hào)支付開發(fā)之現(xiàn)金紅包
這篇文章主要為大家詳細(xì)介紹了java微信公眾號(hào)支付開發(fā)之現(xiàn)金紅包,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-04-04Springboot整合Spring Cloud Kubernetes讀取ConfigMap支持自動(dòng)刷新配置的教程
這篇文章主要介紹了Springboot整合Spring Cloud Kubernetes讀取ConfigMap支持自動(dòng)刷新配置,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09JAVA Swing實(shí)現(xiàn)窗口添加課程信息過程解析
這篇文章主要介紹了JAVA Swing實(shí)現(xiàn)窗口添加課程信息過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10SpringBoot詳細(xì)講解如何創(chuàng)建及刷新Spring容器bean
前面看spring源碼時(shí)可以發(fā)現(xiàn)refresh()方法十分重要。在這個(gè)方法中會(huì)加載beanDefinition,同時(shí)創(chuàng)建bean對(duì)象。那么在springboot中有沒有使用這個(gè)refresh()方法呢2022-06-06Java數(shù)據(jù)結(jié)構(gòu)之鏈表(動(dòng)力節(jié)點(diǎn)之Java學(xué)院整理)
這篇文章主要介紹了Java數(shù)據(jù)結(jié)構(gòu)之鏈表(動(dòng)力節(jié)點(diǎn)之Java學(xué)院整理)的相關(guān)資料,需要的朋友可以參考下2017-04-04