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é)果:
101012121111
Process 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,3printerA prints 1printerA prints 2printerA prints 3PrinterA printed 1,2,3; notify PrinterBwait in printerAprinterB waited for printerA printed 1,2,3printerB prints 4printerB prints 5printerB prints 6notify in printerBnotify end printerBwait end printerAprinterA prints 7printerA prints 8printerA prints 9PrinterA exitsPrinterB 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 printerBsynchronized (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 t1t1 被中斷了!t1 執(zhí)行結(jié)束in t2t2 執(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)注好吧啦網(wǎng)其它相關(guān)文章!
相關(guān)文章:
1. 前端從瀏覽器的渲染到性能優(yōu)化2. 無線標(biāo)記語言(WML)基礎(chǔ)之WMLScript 基礎(chǔ)第1/2頁3. ASP刪除img標(biāo)簽的style屬性只保留src的正則函數(shù)4. 讀大數(shù)據(jù)量的XML文件的讀取問題5. 解析原生JS getComputedStyle6. PHP循環(huán)與分支知識(shí)點(diǎn)梳理7. css代碼優(yōu)化的12個(gè)技巧8. 利用CSS3新特性創(chuàng)建透明邊框三角9. ASP實(shí)現(xiàn)加法驗(yàn)證碼10. ASP基礎(chǔ)入門第三篇(ASP腳本基礎(chǔ))
