Java多线程系列——深入重入锁ReentrantLock
簡(jiǎn)述
ReentrantLock 是一個(gè)可重入的互斥(/獨(dú)占)鎖,又稱(chēng)為“獨(dú)占鎖”。
ReentrantLock通過(guò)自定義隊(duì)列同步器(AQS-AbstractQueuedSychronized,是實(shí)現(xiàn)鎖的關(guān)鍵)來(lái)實(shí)現(xiàn)鎖的獲取與釋放。
其可以完全替代 synchronized 關(guān)鍵字。JDK 5.0 早期版本,其性能遠(yuǎn)好于 synchronized,但 JDK 6.0 開(kāi)始,JDK 對(duì) synchronized 做了大量的優(yōu)化,使得兩者差距并不大。
“獨(dú)占”,就是在同一時(shí)刻只能有一個(gè)線(xiàn)程獲取到鎖,而其它獲取鎖的線(xiàn)程只能處于同步隊(duì)列中等待,只有獲取鎖的線(xiàn)程釋放了鎖,后繼的線(xiàn)程才能夠獲取鎖。
“可重入”,就是支持重進(jìn)入的鎖,它表示該鎖能夠支持一個(gè)線(xiàn)程對(duì)資源的重復(fù)加鎖。
該鎖還支持獲取鎖時(shí)的公平和非公平性選擇。“公平”是指“不同的線(xiàn)程獲取鎖的機(jī)制是公平的”,而“不公平”是指“不同的線(xiàn)程獲取鎖的機(jī)制是非公平的”。
簡(jiǎn)單實(shí)例
import java.util.concurrent.locks.ReentrantLock; /*** Created by zhengbinMac on 2017/3/2.*/ public class ReenterLock implements Runnable{public static ReentrantLock lock = new ReentrantLock();public static int i = 0;public void run() {for (int j = 0;j<100000;j++) {lock.lock(); // lock.lock();try {i++;}finally {lock.unlock(); // lock.unlock(); }}}public static void main(String[] args) throws InterruptedException {ReenterLock reenterLock = new ReenterLock();Thread t1 = new Thread(reenterLock);Thread t2 = new Thread(reenterLock);t1.start();t2.start();t1.join();t2.join();System.out.println(i);} }與 synchronized 相比,重入鎖有著顯示的操作過(guò)程,何時(shí)加鎖,何時(shí)釋放,都在程序員的控制中。
為什么稱(chēng)作是“重入”?這是因?yàn)檫@種鎖是可以反復(fù)進(jìn)入的。將上面代碼中注釋部分去除注釋,也就是連續(xù)兩次獲得同一把鎖,兩次釋放同一把鎖,這是允許的。
注意,獲得鎖次數(shù)與釋放鎖次數(shù)要相同,如果釋放鎖次數(shù)多了,會(huì)拋出 java.lang.IllegalMonitorStateException 異常;如果釋放次數(shù)少了,相當(dāng)于線(xiàn)程還持有這個(gè)鎖,其他線(xiàn)程就無(wú)法進(jìn)入臨界區(qū)。
引出第一個(gè)問(wèn)題:為什么 ReentrantLock 鎖能夠支持一個(gè)線(xiàn)程對(duì)資源的重復(fù)加鎖?
除了簡(jiǎn)單的加鎖、解鎖操作,重入鎖還提供了一些更高級(jí)的功能,下面結(jié)合實(shí)例進(jìn)行簡(jiǎn)單介紹:
中斷響應(yīng)(lockInterruptibly)
對(duì)于 synchronized 來(lái)說(shuō),如果一個(gè)線(xiàn)程在等待鎖,那么結(jié)果只有兩種情況,獲得這把鎖繼續(xù)執(zhí)行,或者線(xiàn)程就保持等待。
而使用重入鎖,提供了另一種可能,這就是線(xiàn)程可以被中斷。也就是在等待鎖的過(guò)程中,程序可以根據(jù)需要取消對(duì)鎖的需求。
下面的例子中,產(chǎn)生了死鎖,但得益于鎖中斷,最終解決了這個(gè)死鎖:
1 import java.util.concurrent.locks.ReentrantLock; 2 /** 3 * Created by zhengbinMac on 2017/3/2. 4 */ 5 public class IntLock implements Runnable{ 6 public static ReentrantLock lock1 = new ReentrantLock(); 7 public static ReentrantLock lock2 = new ReentrantLock(); 8 int lock; 9 /** 10 * 控制加鎖順序,產(chǎn)生死鎖 11 */ 12 public IntLock(int lock) { 13 this.lock = lock; 14 } 15 public void run() { 16 try { 17 if (lock == 1) { 18 lock1.lockInterruptibly(); // 如果當(dāng)前線(xiàn)程未被 中斷,則獲取鎖。 19 try { 20 Thread.sleep(500); 21 } catch (InterruptedException e) { 22 e.printStackTrace(); 23 } 24 lock2.lockInterruptibly(); 25 System.out.println(Thread.currentThread().getName()+",執(zhí)行完畢!"); 26 } else { 27 lock2.lockInterruptibly(); 28 try { 29 Thread.sleep(500); 30 } catch (InterruptedException e) { 31 e.printStackTrace(); 32 } 33 lock1.lockInterruptibly(); 34 System.out.println(Thread.currentThread().getName()+",執(zhí)行完畢!"); 35 } 36 } catch (InterruptedException e) { 37 e.printStackTrace(); 38 } finally { 39 // 查詢(xún)當(dāng)前線(xiàn)程是否保持此鎖。 40 if (lock1.isHeldByCurrentThread()) { 41 lock1.unlock(); 42 } 43 if (lock2.isHeldByCurrentThread()) { 44 lock2.unlock(); 45 } 46 System.out.println(Thread.currentThread().getName() + ",退出。"); 47 } 48 } 49 public static void main(String[] args) throws InterruptedException { 50 IntLock intLock1 = new IntLock(1); 51 IntLock intLock2 = new IntLock(2); 52 Thread thread1 = new Thread(intLock1, "線(xiàn)程1"); 53 Thread thread2 = new Thread(intLock2, "線(xiàn)程2"); 54 thread1.start(); 55 thread2.start(); 56 Thread.sleep(1000); 57 thread2.interrupt(); // 中斷線(xiàn)程2 58 } 59 } View Code上述例子中,線(xiàn)程 thread1 和 thread2 啟動(dòng)后,thread1 先占用 lock1,再占用 lock2;thread2 反之,先占 lock2,后占 lock1。這便形成 thread1 和 thread2 之間的相互等待。
代碼 56 行,main 線(xiàn)程處于休眠(sleep)狀態(tài),兩線(xiàn)程此時(shí)處于死鎖的狀態(tài),代碼 57 行 thread2 被中斷(interrupt),故 thread2 會(huì)放棄對(duì) lock1 的申請(qǐng),同時(shí)釋放已獲得的 lock2。這個(gè)操作導(dǎo)致 thread1 順利獲得 lock2,從而繼續(xù)執(zhí)行下去。
執(zhí)行代碼,輸出如下:
鎖申請(qǐng)等待限時(shí)(tryLock)
除了等待外部通知(中斷操作 interrupt )之外,限時(shí)等待也可以做到避免死鎖。
通常,無(wú)法判斷為什么一個(gè)線(xiàn)程遲遲拿不到鎖。也許是因?yàn)楫a(chǎn)生了死鎖,也許是產(chǎn)生了饑餓。但如果給定一個(gè)等待時(shí)間,讓線(xiàn)程自動(dòng)放棄,那么對(duì)系統(tǒng)來(lái)說(shuō)是有意義的。可以使用 tryLock() 方法進(jìn)行一次限時(shí)的等待。
1 import java.util.concurrent.TimeUnit; 2 import java.util.concurrent.locks.ReentrantLock; 3 /** 4 * Created by zhengbinMac on 2017/3/2. 5 */ 6 public class TimeLock implements Runnable{ 7 public static ReentrantLock lock = new ReentrantLock(); 8 public void run() { 9 try { 10 if (lock.tryLock(5, TimeUnit.SECONDS)) { 11 Thread.sleep(6 * 1000); 12 }else { 13 System.out.println(Thread.currentThread().getName()+" get Lock Failed"); 14 } 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 }finally { 18 // 查詢(xún)當(dāng)前線(xiàn)程是否保持此鎖。 19 if (lock.isHeldByCurrentThread()) { 20 System.out.println(Thread.currentThread().getName()+" release lock"); 21 lock.unlock(); 22 } 23 } 24 } 25 /** 26 * 在本例中,由于占用鎖的線(xiàn)程會(huì)持有鎖長(zhǎng)達(dá)6秒,故另一個(gè)線(xiàn)程無(wú)法再5秒的等待時(shí)間內(nèi)獲得鎖,因此請(qǐng)求鎖會(huì)失敗。 27 */ 28 public static void main(String[] args) { 29 TimeLock timeLock = new TimeLock(); 30 Thread t1 = new Thread(timeLock, "線(xiàn)程1"); 31 Thread t2 = new Thread(timeLock, "線(xiàn)程2"); 32 t1.start(); 33 t2.start(); 34 } 35 } View Code上述例子中,由于占用鎖的線(xiàn)程會(huì)持有鎖長(zhǎng)達(dá) 6 秒,故另一個(gè)線(xiàn)程無(wú)法在 5 秒的等待時(shí)間內(nèi)獲得鎖,因此,請(qǐng)求鎖失敗。
ReentrantLock.tryLock()方法也可以不帶參數(shù)直接運(yùn)行。這種情況下,當(dāng)前線(xiàn)程會(huì)嘗試獲得鎖,如果鎖并未被其他線(xiàn)程占用,則申請(qǐng)鎖成功,立即返回 true。否則,申請(qǐng)失敗,立即返回 false,當(dāng)前線(xiàn)程不會(huì)進(jìn)行等待。這種模式不會(huì)引起線(xiàn)程等待,因此也不會(huì)產(chǎn)生死鎖。
公平鎖
默認(rèn)情況下,鎖的申請(qǐng)都是非公平的。也就是說(shuō),如果線(xiàn)程 1 與線(xiàn)程 2,都申請(qǐng)獲得鎖 A,那么誰(shuí)獲得鎖不是一定的,是由系統(tǒng)在等待隊(duì)列中隨機(jī)挑選的。這就好比,買(mǎi)票的人不排隊(duì),售票姐姐只能隨機(jī)挑一個(gè)人賣(mài)給他,這顯然是不公平的。而公平鎖,它會(huì)按照時(shí)間的先后順序,保證先到先得。公平鎖的特點(diǎn)是:不會(huì)產(chǎn)生饑餓現(xiàn)象。
重入鎖允許對(duì)其公平性進(jìn)行設(shè)置。構(gòu)造函數(shù)如下:
public ReentrantLock(boolean fair)下面舉例來(lái)說(shuō)明,公平鎖與非公平鎖的不同:
1 import java.util.concurrent.locks.ReentrantLock; 2 /** 3 * Created by zhengbinMac on 2017/3/2. 4 */ 5 public class FairLock implements Runnable{ 6 public static ReentrantLock fairLock = new ReentrantLock(true); 7 8 public void run() { 9 while (true) { 10 try { 11 fairLock.lock(); 12 System.out.println(Thread.currentThread().getName()+",獲得鎖!"); 13 }finally { 14 fairLock.unlock(); 15 } 16 } 17 } 18 public static void main(String[] args) { 19 FairLock fairLock = new FairLock(); 20 Thread t1 = new Thread(fairLock, "線(xiàn)程1"); 21 Thread t2 = new Thread(fairLock, "線(xiàn)程2"); 22 t1.start();t2.start(); 23 } 24 } View Code修改重入鎖是否公平,觀察輸出結(jié)果,如果公平,輸出結(jié)果始終為兩個(gè)線(xiàn)程交替的獲得鎖,如果是非公平,輸出結(jié)果為一個(gè)線(xiàn)程占用鎖很長(zhǎng)時(shí)間,然后才會(huì)釋放鎖,另個(gè)線(xiàn)程才能執(zhí)行。
引出第二個(gè)問(wèn)題:為什么公平鎖例子中出現(xiàn),公平鎖線(xiàn)程是不斷切換的,而非公平鎖出現(xiàn)同一線(xiàn)程連續(xù)獲取鎖的情況?
結(jié)合源碼再看“重入”
何為重進(jìn)入(重入)?
重進(jìn)入是指任意線(xiàn)程在獲取到鎖之后能夠再次獲取該鎖而不會(huì)被鎖阻塞,該特性的實(shí)現(xiàn)需要解決以下兩個(gè)問(wèn)題:
- 線(xiàn)程再次獲取鎖:鎖需要去識(shí)別獲取鎖的線(xiàn)程是否為當(dāng)前占據(jù)鎖的線(xiàn)程,如果是,則再次成功獲取。
- 鎖的最終釋放。線(xiàn)程重復(fù) n 次獲取了鎖,隨后在第 n 次釋放該鎖后,其它線(xiàn)程能夠獲取到該鎖。鎖的最終釋放要求鎖對(duì)于獲取進(jìn)行計(jì)數(shù)自增,計(jì)數(shù)表示當(dāng)前鎖被重復(fù)獲取的次數(shù),而鎖被釋放時(shí),計(jì)數(shù)自減,當(dāng)計(jì)數(shù)等于 0 時(shí)表示鎖已經(jīng)成功釋放。
以非公平鎖源碼分析:
獲取:
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false; }acquireQueued 方法增加了再次獲取同步狀態(tài)的處理邏輯:通過(guò)判斷當(dāng)前線(xiàn)程是否為獲取鎖的線(xiàn)程,來(lái)決定獲取操作是否成功,如果獲取鎖的線(xiàn)程再次請(qǐng)求,則將同步狀態(tài)值進(jìn)行增加并返回 true,表示獲取同步狀態(tài)成功。
成功獲取鎖的線(xiàn)程再次獲取鎖,只是增加了同步狀態(tài)值,也就是要求 ReentrantLock 在釋放同步狀態(tài)時(shí)減少同步狀態(tài)值,釋放鎖源碼如下:
如果鎖被獲取 n 次,那么前 (n-1) 次 tryRelease(int releases) 方法必須返回 false,只有同步狀態(tài)完全釋放了,才能返回 true。該方法將同步狀態(tài)是否為 0 作為最終釋放的條件,當(dāng)同步狀態(tài)為 0 時(shí),將占有線(xiàn)程設(shè)置為 null,并返回 true,表示釋放成功。
通過(guò)對(duì)獲取與釋放的分析,就可以解釋,以上兩個(gè)例子中出現(xiàn)的兩個(gè)問(wèn)題:為什么 ReentrantLock 鎖能夠支持一個(gè)線(xiàn)程對(duì)資源的重復(fù)加鎖?為什么公平鎖例子中出現(xiàn),公平鎖線(xiàn)程是不斷切換的,而非公平鎖出現(xiàn)同一線(xiàn)程連續(xù)獲取鎖的情況?
- 為什么支持重復(fù)加鎖?因?yàn)樵创a中用變量 c 來(lái)保存當(dāng)前鎖被獲取了多少次,故在釋放時(shí),對(duì) c 變量進(jìn)行減操作,只有 c 變量為 0 時(shí),才算鎖的最終釋放。所以可以 lock 多次,同時(shí) unlock 也必須與 lock 同樣的次數(shù)。
- 為什么非公平鎖出現(xiàn)同一線(xiàn)程連續(xù)獲取鎖的情況?tryAcquire 方法中增加了再次獲取同步狀態(tài)的處理邏輯。
小結(jié)
對(duì)上面ReentrantLock的幾個(gè)重要方法整理如下:
- lock():獲得鎖,如果鎖被占用,進(jìn)入等待。
- lockInterruptibly():獲得鎖,但優(yōu)先響應(yīng)中斷。
- tryLock():嘗試獲得鎖,如果成功,立即放回 true,反之失敗返回 false。該方法不會(huì)進(jìn)行等待,立即返回。
- tryLock(long time, TimeUnit unit):在給定的時(shí)間內(nèi)嘗試獲得鎖。
- unLock():釋放鎖。
對(duì)于其實(shí)現(xiàn)原理,下篇博文將詳細(xì)分析,其主要包含三個(gè)要素:
- 原子狀態(tài):原子狀態(tài)有 CAS(compareAndSetState) 操作來(lái)存儲(chǔ)當(dāng)前鎖的狀態(tài),判斷鎖是否有其他線(xiàn)程持有。
- 等待隊(duì)列:所有沒(méi)有請(qǐng)求到鎖的線(xiàn)程,會(huì)進(jìn)入等待隊(duì)列進(jìn)行等待。待有線(xiàn)程釋放鎖后,系統(tǒng)才能夠從等待隊(duì)列中喚醒一個(gè)線(xiàn)程,繼續(xù)工作。詳見(jiàn):隊(duì)列同步器——AQS(待更新)
- 阻塞原語(yǔ) park() 和 unpark(),用來(lái)掛起和恢復(fù)線(xiàn)程。沒(méi)有得到鎖的線(xiàn)程將會(huì)被掛起。關(guān)于阻塞原語(yǔ),詳見(jiàn):線(xiàn)程阻塞工具類(lèi)——LockSupport(待更新)。
參考資料
[1] Java并發(fā)編程的藝術(shù), 5.3 - 重入鎖
[2] 實(shí)戰(zhàn)Java高并發(fā)程序設(shè)計(jì), 3.1.1 - synchronized的功能擴(kuò)展:重入鎖
轉(zhuǎn)載于:https://www.cnblogs.com/zhengbin/p/6503412.html
總結(jié)
以上是生活随笔為你收集整理的Java多线程系列——深入重入锁ReentrantLock的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Android开发启动未注册的activ
- 下一篇: Java之—hutool工具类二维码生成