顯式鎖
Java 為同步提供了兩種鎖宛徊,一種是語言特性提供的內(nèi)置鎖歧杏,即 synchronized 關(guān)鍵字,詳見 Java多線程學(xué)習(xí)之對(duì)象及變量的并發(fā)訪問 影晓;還有一種是 JDK 提供的顯式鎖镰吵。本文我們來介紹顯式鎖:
使用 ReentrantLock 類
java.util.concurrent.locks(J.U.C)包中提供了可重入的顯式鎖(ReentrantLock),需要顯式進(jìn)行 lock 以及 unlock 操作挂签。ReentrantLock 和 synchronized 一樣都能實(shí)現(xiàn)同步疤祭,且都是可重入的。但 ReentrantLock 在擴(kuò)展功能上更為強(qiáng)大饵婆,使用上更加靈活勺馆。
用 ReentrantLock 保護(hù)代碼塊的基本結(jié)構(gòu)如下 :
lock.lock() ; // lock是一個(gè)ReentrantLock對(duì)象,lock方法上鎖
try
{
//被同步的臨界區(qū)
}
finally
{
lock.unlock(); //unlock方法解鎖侨核,放在finally子句中草穆,即使拋出異常也要解鎖
}
這一結(jié)構(gòu)確保任何時(shí)刻只有一個(gè)線程進(jìn)入臨界區(qū)。一旦一個(gè)線程封鎖了鎖對(duì)象搓译,其他任何線程都無法通過 lock 語句悲柱。當(dāng)其他線程調(diào)用 lock 時(shí),它們被阻塞侥衬,直到第一個(gè)線程釋放鎖對(duì)象。
注意:
1.把解鎖操作置于 finally 子句之內(nèi)是至關(guān)重要的跑芳。如果在臨界區(qū)的代碼拋出異常轴总,鎖必須被釋放。否則博个,其他線程將永遠(yuǎn)阻塞怀樟。
2.如果使用鎖,就不能使用帶資源的 try 語句盆佣。首先往堡,解鎖方法名不是 close。不過共耍,即使將它重命名虑灰,帶資源的 try 語句也無法正常工作。它的首部希望聲明一個(gè)新變量痹兜。但是如果使用一個(gè)鎖穆咐,你可能想使用多個(gè)線程共享的那個(gè)變量 (而不是新變量)。
使用 ReentrantLock 實(shí)現(xiàn)同步:測(cè)試1
MyService.java
MyThread.java
Run.java
運(yùn)行結(jié)果
使用 ReentrantLock 實(shí)現(xiàn)同步:測(cè)試2
ConditionTestMoreMethod.java
第一組線程類
第二組線程類
Run.java
運(yùn)行結(jié)果
分析
實(shí)驗(yàn)說明,調(diào)用lock.lock()的代碼相當(dāng)于持有了“對(duì)象監(jiān)視器”对湃,其他線程只有等待鎖被釋放時(shí)再次爭(zhēng)搶崖叫,效果和使用 synchronized 關(guān)鍵字一樣。
條件對(duì)象
下面是 Condition 對(duì)象常用的API
Condition 對(duì)象的await()拍柒,await(long time, TimeUnit unit), signal()心傀,signalAll()方法的功能分別對(duì)應(yīng)Object類的wait(),wait(long timeout), notify()拆讯,notifyAll()方法脂男。
和Object類的wait(),wait(long timeout), notify()往果,notifyAll()方法必須在同步方法或同步代碼塊中使用類似疆液,Condition 對(duì)象的await(),await(long time, TimeUnit unit), signal()陕贮,signalAll()方法也必須在lock.lock()和lock.unlock()構(gòu)成的臨界區(qū)內(nèi)使用堕油,否則會(huì)拋出IllegalMonitorStateException!
await() 會(huì)使當(dāng)前線程釋放鎖肮之,如果一個(gè)線程在 await 時(shí)調(diào)用 interrupt() 會(huì)拋出 InterruptedException掉缺。
使用 Condition 實(shí)現(xiàn)等待 / 通知:錯(cuò)誤用法與解決
MyService.java
ThreadA.java
Run.java
運(yùn)行結(jié)果
分析
因?yàn)闊o監(jiān)視器對(duì)象,所以程序拋出了異常戈擒。解決方法是在 condition.await() 方法調(diào)用前調(diào)用 lock.lock() 獲得同步監(jiān)視器眶明。
修改MyService.java
MyThreadA.java 和 Run.java
運(yùn)行結(jié)果
正確使用 Condition 實(shí)現(xiàn)等待 / 通知
MyService.java
ThreadA.java
Run.java
運(yùn)行結(jié)果
使用多個(gè) Condition 實(shí)現(xiàn)通知部分線程:錯(cuò)誤用法
MyService.java
ThreadA.java 和 ThreadB.java
Run.java
運(yùn)行結(jié)果
我們發(fā)現(xiàn)A、B線程都被喚醒了筐高,如何實(shí)現(xiàn)喚醒指定的一個(gè)線程呢搜囱?這時(shí)候就需要使用多個(gè) Condition 對(duì)象了,可以先對(duì)線程進(jìn)行分組柑土,再喚醒指定組內(nèi)的線程蜀肘。
使用多個(gè) Condition 實(shí)現(xiàn)通知部分線程:正確用法
MyService.java
ThreadA.java 和 ThreadB.java
Run.java
運(yùn)行結(jié)果
只有A被喚醒了。我們可以發(fā)現(xiàn)稽屏,同一個(gè) Condition 對(duì)象只能喚醒它 await() 的線程扮宠,可以把同一個(gè) Condition 對(duì)象 await() 的線程分為一組,該 Condition 對(duì)象的 signal() 是在該組等待線程中隨機(jī)選擇一個(gè)喚醒狐榔,而 signalAll() 是喚醒該組中所有的等待線程坛增。
實(shí)現(xiàn)生產(chǎn)者 / 消費(fèi)者模式:一對(duì)一交替打印
MyService.java
ThreadA.java 和 ThreadB.java
Run.java
實(shí)現(xiàn)生產(chǎn)者 / 消費(fèi)者模式:多對(duì)多交替打印
為了防止假死,把 signal() 改為 signalAll() 即可薄腻。
公平鎖與非公平鎖
Service.java
RunFair.java
運(yùn)行結(jié)果
打印結(jié)果基本呈有序狀態(tài)收捣,這就是公平鎖的特點(diǎn)。
再來看看非公平鎖庵楷,只要修改傳入的 true 參數(shù)為 false 即可坏晦。
運(yùn)行結(jié)果
非公平鎖的運(yùn)行結(jié)果基本上是亂序的,說明先啟動(dòng)的線程不一定先獲得鎖。
方法 getHoldCount() 昆婿、getQueueLength()球碉、getWaitQueueLength() 的測(cè)試
1)int getHoldCount() 方法的作用是查詢當(dāng)前線程保持此鎖定的個(gè)數(shù),也就是調(diào)用 lock() 方法的次數(shù)仓蛆。
每個(gè)鎖關(guān)聯(lián)一個(gè)線程持有者和一個(gè)計(jì)數(shù)器睁冬。當(dāng)計(jì)數(shù)器為0時(shí)表示該鎖沒有被任何線程持有,那么任何線程都都可能獲得該鎖而調(diào)用相應(yīng)方法看疙。當(dāng)一個(gè)線程請(qǐng)求成功后豆拨,JVM會(huì)記下持有鎖的線程,并將計(jì)數(shù)器計(jì)加1能庆。此時(shí)其他線程請(qǐng)求該鎖施禾,則必須等待。而該持有鎖的線程如果再次請(qǐng)求這個(gè)鎖搁胆,就可以再次拿到這個(gè)鎖弥搞,同時(shí)計(jì)數(shù)器會(huì)遞增。當(dāng)線程退出一個(gè)ReentrantLock鎖住的代碼塊時(shí)渠旁,計(jì)數(shù)器會(huì)遞減攀例,直到計(jì)數(shù)器為0才釋放該鎖。getHoldCount() 方法返回的就是這個(gè)計(jì)數(shù)器的值顾腊。
Service.java
Run.java
運(yùn)行結(jié)果
2)方法 int getQueueLength() 的作用是返回正等待獲取此鎖定線程的估計(jì)數(shù)
比如有 5 個(gè)線程粤铭,1 個(gè)線程首先執(zhí)行 await() 方法,那么在調(diào)用 getQueueLength() 方法后返回值是 4杂靶,說明有 4 個(gè)線程同時(shí)在等待 lock 的釋放梆惯。
Service.java
Run.java
運(yùn)行結(jié)果
3)方法 int getWaitQueueLength(Condition condition) 的作用是返回同一個(gè) Condition 對(duì)象的等待線程數(shù)。
如果同時(shí)開啟了5個(gè)線程吗垮,都調(diào)用了await()方法垛吗,并且它們是用的同一個(gè)Condition 對(duì)象,那么在調(diào)用該方法返回值為5抱既。
Service.java
Run.java
方法 hasQueuedThread()职烧、hasQueuedThreads() 和 hasWaiters() 的測(cè)試
1)方法 boolean hasQueuedThread(Thread thread) 的作用是查詢指定的線程是否正在等待獲取此鎖定扁誓。方法 boolean hasQueuedThreads() 的作用時(shí)查詢是否有線程正在等待獲取此鎖定防泵。
Service.java
Run.java
運(yùn)行結(jié)果
2)方法 boolean hasWaiters(Condition condition) 的作用是查詢是否有線程正在等待與此鎖定有關(guān)的 condition 條件。
Service.java
Run.java
運(yùn)行結(jié)果
方法 isFair()蝗敢、isHeldByCurrentThread() 和 isLocked() 的測(cè)試
1)方法 isFair() 的作用是判斷是不是公平鎖
lock.isFair() 可以判斷 lock 是否是公平鎖捷泞,默認(rèn)情況下,ReentrantLock 類使用的是非公平鎖寿谴。
2)方法 boolean isHeldByCurrentThread() 的作用是查詢當(dāng)前線程是否保持此鎖定锁右。
Service.java
Run.java
運(yùn)行結(jié)果
3)方法 boolean isLocked() 的作用是查詢此鎖定是否由任意線程保持。
Service.java
Run.java
方法 lockInterruptibly()、tryLock() 和 tryLock(long timeout, TimeUnit unit) 的測(cè)試
1)方法 void lockInterruptibly() 的作用是:如果當(dāng)前線程未被中斷咏瑟,則獲取鎖定,如果已經(jīng)被中斷則出現(xiàn)異常。
MyService.java
Run.java
運(yùn)行結(jié)果
沒有出現(xiàn)異常纳账,說明即使線程被 interrupt() 中斷了幅聘,執(zhí)行 lock() 也不出現(xiàn)異常。
把 MyService.java 中的 lock.lock() 修改為 lock.lockInterruptibly()
運(yùn)行結(jié)果
線程被中斷后調(diào)用 lockInterruptibly() 會(huì)拋出 InterruptedException余寥。
2)方法 boolean tryLock() 的作用是领铐,僅在調(diào)用時(shí)鎖定未被另一個(gè)線程保持的情況下,才獲取該鎖定宋舷。
MyService.java
Run.java
運(yùn)行結(jié)果
3)方法 boolean tryLock(long timeout, TimeUnit unit) 的作用是绪撵,如果鎖定在給定等待時(shí)間內(nèi)沒有被另一個(gè)線程保持,且當(dāng)前線程未被中斷祝蝠,則獲取該鎖定音诈。
MyService.java
Run.java
運(yùn)行時(shí)間
awaitUninterruptibly() 的使用
awaitUninterruptibly() 和 await() 方法用法基本相同,只是它并不響應(yīng)中斷請(qǐng)求续膳,即在調(diào)用 awaitUninterruptibly() 時(shí)調(diào)用 interrupt() 不會(huì)拋出 InterruptedException改艇。
MyService.java
MyThread.java
Run.java
運(yùn)行結(jié)果
修改 Service.java 中的 await() 為 awaitUninterruptibly(), 運(yùn)行結(jié)果如下
awaitUntil() 的使用
awaitUntil() 設(shè)定一個(gè)超時(shí)間隔,如果在規(guī)定時(shí)間內(nèi)沒有被通知或中斷坟岔,線程將被喚醒谒兄。
ThreadA.java 和 ThreadB.java
Service.java
Run1.java
運(yùn)行結(jié)果
線程等待 10 秒后自動(dòng)喚醒自己。
Run2.java
使用 Condition 實(shí)現(xiàn)順序執(zhí)行
使用 Condition 對(duì)象可以對(duì)線程執(zhí)行的業(yè)務(wù)進(jìn)行排序規(guī)劃社付。指定喚醒特定的線程承疲,比 Object 的 notify() 更具精準(zhǔn)可控性。
Run.java
運(yùn)行結(jié)果
ReentrantLock與synchronized比較
1. 鎖的實(shí)現(xiàn)
synchronized 是 JVM 實(shí)現(xiàn)的鸥咖,而 ReentrantLock 是 JDK 實(shí)現(xiàn)的燕鸽。
2. 性能
新版本 Java 對(duì) synchronized 進(jìn)行了很多優(yōu)化,例如自旋鎖等啼辣,synchronized 與 ReentrantLock 大致相同啊研。
3. 等待可中斷
當(dāng)持有鎖的線程長(zhǎng)期不釋放鎖的時(shí)候,正在等待的線程可以選擇放棄等待鸥拧,改為處理其他事情党远。ReentrantLock 可中斷,而 synchronized 不行富弦。
4. 公平鎖
公平鎖是指多個(gè)線程在等待同一個(gè)鎖時(shí)沟娱,必須按照申請(qǐng)鎖的時(shí)間順序來依次獲得鎖。synchronized 中的鎖是非公平的腕柜,ReentrantLock 默認(rèn)情況下也是非公平的济似,但是也可以是公平的矫废。
5. 鎖綁定多個(gè)條件
一個(gè) ReentrantLock 可以同時(shí)綁定多個(gè) Condition 對(duì)象。從而可以靈活地對(duì)線程進(jìn)行分組阻塞和喚醒砰蠢。
使用選擇
除非需要使用 ReentrantLock 的高級(jí)功能蓖扑,否則優(yōu)先使用 synchronized。這是因?yàn)?synchronized 是 JVM 實(shí)現(xiàn)的一種鎖機(jī)制台舱,JVM 原生地支持它赵誓,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用擔(dān)心沒有釋放鎖而導(dǎo)致死鎖問題柿赊,因?yàn)?JVM 會(huì)確保鎖的釋放俩功。而如果使用 ReentrantLock 必須把釋放鎖的代碼放在finally子句中,使線程異常終止時(shí)也能釋放鎖碰声。
使用 ReentrantReadWriteLock 類
類 ReentrantReadWriteLock 的使用:讀讀共享
Service.java
Run.java
ThreadA.java 和 ThreadB.java
運(yùn)行結(jié)果
我們發(fā)現(xiàn)兩個(gè)線程同時(shí)獲得讀鎖诡蜓,說明讀操作是共享的。
類 ReentrantReadWriteLock 的使用:寫寫互斥
Service.java
運(yùn)行結(jié)果
我們發(fā)現(xiàn)第二個(gè)線程獲得寫鎖的時(shí)間比第一個(gè)線程晚了 10 秒胰挑,說明寫操作是互斥的蔓罚。
類 ReentrantReadWriteLock 的使用:讀寫互斥
Service.java
Run.java
運(yùn)行結(jié)果
實(shí)驗(yàn)說明讀寫操作是互斥的。
類 ReentrantReadWriteLock 的使用:寫讀互斥
同理寫讀操作也是互斥的瞻颂。
Run.java
運(yùn)行結(jié)果
總結(jié):“讀寫”豺谈、“寫讀”、“寫寫”都是同步的贡这、互斥的茬末;"讀讀"是異步的、共享的盖矫。