原文地址:https://www.relaxheart.cn/to/master/blog?uuid=81
靈活的重入鎖
重入鎖ReenterLock我把它理解為是synchronized的增強版,因為重入鎖可以完全替代synchronized關(guān)鍵字。在JDK 5.0的早期版本中,重入鎖的性能遠遠好于synchronized,但從JDK6.0開始對synchronized做了大量的優(yōu)化工作,是的兩者的性能差距并不大了。
重入鎖使用java.util.concurrent.locks.ReentrantLock類來實現(xiàn)。下面是一段最見到那的重入鎖使用案例:
/**
* @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
* @Date: 2019-5-3 0003 22:43
* @Description: 重入鎖ReentrantLock的簡單使用案例
*/
public class ReenterLock implements Runnable {
// 重入鎖
public static ReentrantLock lock = new ReentrantLock();
// 共享變量i
public static int i = 0;
@Override
public void run() {
for (int j = 0; j<1000000; j++){
// 加鎖
lock.lock();
try{
i++;
} finally {
// 釋放鎖
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 = " + i);
}
}
上述代碼中我們使用了重入鎖保護臨界區(qū)資源i,確保多線程對i操作的安全性。從這段代碼可以看到,與synchronized相比,沖入鎖有著顯示的操作過程。開發(fā)人員必須手動指定何時加鎖,何時釋放鎖。因此重入鎖對邏輯控制的靈活性也遠遠好于synchronized。但值得注意的是,在退出臨界區(qū)時,必須記得釋放鎖,否則其他線程就沒有機會在訪問臨界區(qū)了。
有些同學(xué)可能對重入鎖這“重入”二字有點糊涂,鎖就是鎖嘛,為什么叫重入鎖呢。之所以這么叫是因為這種鎖時可以反復(fù)進入的。當然,這里的反復(fù)僅僅局限于一個線程。我們可以修改上述代碼中加鎖的代碼段:
@Override
public void run() {
for (int j = 0; j<1000000; j++){
// 加鎖
lock.lock();
lock.lock();
try{
i++;
} finally {
// 釋放鎖
lock.unlock();
lock.unlock();
}
}
}
在這種情況下一個線程會連續(xù)兩次獲得同一把鎖。如果不允許這么操作,那同一個線程在第二次獲得鎖的時候?qū)妥约寒a(chǎn)生死鎖(線程會卡死在第2次獲取鎖的過程中)。需要注意的是如果一個線程可以多此獲得鎖(即多次lock.lock()),那么相應(yīng)的也要對應(yīng)次數(shù)的釋放鎖(lock.unlock()),相反,如果釋放次數(shù)少了,相當于當前線程還是持有鎖,那其他線程也無法進入臨界區(qū)。
除了上述靈活性外,重入鎖還提供了一些高級功能。比如,重入鎖可以提供終端處理的能力。
中斷響應(yīng)
對于synchronized來說,如果一個線程在等待鎖,那么結(jié)果只有兩種:要么持有鎖,要么保持等待。而使用重入鎖則提供了另外一種可能:那就是線程可以被中斷。也就是在等待鎖的過程中,程序可以根據(jù)需要取消對鎖的請求。有些時候,這么做是非常有必要的。比如:如果你和你的朋友約好一起去打球,如果你等了半小時朋友還沒到,突然接到他的電話有時來不來了,那么你一定就掃興的打道回府或者自己走了。中斷正是提供了一套類似的機制,如果一個線程正在等待鎖,那么它依然可以收到一個通知,被告知無需在等待,可以停止工作了。這種情況對于處理死鎖是有一定幫助的。
下面的代碼產(chǎn)生了一個死鎖,但得益于鎖中斷,我們可以很輕易的解決這個死鎖:
/**
* @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
* @Date: 2019-5-3 0003 23:05
* @Description: 重入鎖高級特性:鎖中斷
*/
public class IntLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
/**
* 控制加鎖順序,方便構(gòu)造死鎖
* @param lock
*/
public IntLock(int lock){
this.lock = lock;
}
@Override
public void run() {
try{
if (lock == 1){
lock1.lockInterruptibly();
Thread.sleep(500);
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
Thread.sleep(500);
lock1.lockInterruptibly();
}
} catch (InterruptedException e){
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()){
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()){
lock2.unlock();
}
System.out.println(Thread.currentThread().getId()+":線程退出");
}
}
public static void main(String[] args) throws InterruptedException {
IntLock intLock1 = new IntLock(1);
IntLock intLock2 = new IntLock(2);
Thread t1 = new Thread(intLock1);
Thread t2 = new Thread(intLock2);
t1.start(); t2.start();
Thread.sleep(1000);
t2.interrupt();
}
}
線程t1和t2啟動后, t1先占用lock1,在占用lock2; t2先占用lock2,在請求lock1.因此很容易形成t1和t2之間的相互等待。在這里,對鎖的請求,統(tǒng)一使用lockInterruptibly()方法。這是一個可以對中斷進行響應(yīng)的鎖申請動作,即在等待鎖的過程中,可以響應(yīng)中斷。
執(zhí)行結(jié)果:
java.lang.InterruptedException
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:944)
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1263)
at java.base/java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:317)
at cn.relaxheart.designModel.reenterLock.IntLock.run(IntLock.java:34)
at java.base/java.lang.Thread.run(Thread.java:835)
14:線程退出
13:線程退出
可以看出,中斷后,兩個線程雙雙退出。但真正完成工作的只有t1.而t2線程則放棄其任務(wù)直接退出,釋放資源。
鎖申請等待時間
除了等待外部通知之外,要避免四速還有另外一種方法,那就是限時等待。依然以約朋友打球為例,如果朋友遲遲不來,又無法聯(lián)系到他。那么,在等待1~2小時后,我想大部分人都會掃興離開。對線程來說也是這樣。通常,我們無法判斷為什么一個線程遲遲拿不到鎖。也許是因為死鎖了,也許是因為產(chǎn)生了饑餓。但如果給定一個等待時間,讓線程自動放棄,那么對系統(tǒng)來說是又意義的。我們可以使用tryLock()方法進行一次顯示等待。
下面代碼展示了限時等待鎖的使用:
/**
* @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
* @Date: 2019-5-3 0003 23:34
* @Description: 重入鎖高級特性:限時等待鎖
*/
public class TimeLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try{
if (lock.tryLock(5, TimeUnit.SECONDS)){
Thread.sleep(6000);
} else {
System.out.println("get lock failed");
}
} catch (InterruptedException e){
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
public static void main(String[] args) {
TimeLock timeLock = new TimeLock();
Thread t1 = new Thread(timeLock);
Thread t2 = new Thread(timeLock);
t1.start();
t2.start();
}
}
在這里,tryLock()方法接收兩個參數(shù),一個表示等待時長,另外一個表示即使單位。這里的單位設(shè)置為秒,時長為5,表示線程在這個鎖請求中,最多等待5秒。如果超過5秒還沒有得到鎖,就會返回false。如果成功獲得鎖,則返回true。
在這個例子中,由于占用鎖的線程會持有鎖長達6秒,故另一個線程無法在5秒的等待時間內(nèi)獲得鎖,因此,請求鎖會失敗。
ReentrantLock.tryLock()方法也可以不帶參數(shù)直接運行。在這種情況下,當前線程會嘗試獲得鎖,如果鎖并未被其他線程占用,則申請鎖會成功,并立即返回true。如果鎖被其他線程占用,則當前線程不會進行等待,而是立即返回false。這種模式不會引起線程等待,因此也不會產(chǎn)生死鎖。下面演示了這種使用方式:
/**
* @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
* @Date: 2019-5-3 0003 23:53
* @Description: 無描述信息
*/
public class TryLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public TryLock(int lock){
this.lock = lock;
}
@Override
public void run() {
if (lock == 1) {
while (true){
if (lock1.tryLock()){
try {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
if (lock2.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + ":My Job done");
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
}
} else {
while (true){
if (lock2.tryLock()){
try {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
if (lock1.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + ":My Job done");
} finally {
lock1.unlock();
}
}
} finally {
lock2.unlock();
}
}
}
}
}
public static void main(String[] args) {
TryLock tryLock1 = new TryLock(1);
TryLock tryLock2 = new TryLock(2);
Thread t1 = new Thread(tryLock1);
Thread t2 = new Thread(tryLock2);
t1.start();
t2.start();
}
}
上述代碼中,采用了非常容易死鎖的加鎖順序。也就是先讓t1獲得lock1, 再讓t2獲得lock2,接著做反向請求,讓t1申請lock2, t2申請lock1.在一般情況下,這會導(dǎo)致t1 和 t2相互等待,從而引起死鎖。
但是使用tryLock()后,這種情況就大大改善了。由于線程不會傻傻地等待,而是不停地嘗試,因此,只要執(zhí)行足夠長的時間,線程總是會得到所需要的資源,從而正常執(zhí)行(這里以線程同時獲得lock1,lock2兩把鎖,作為其可以正常執(zhí)行的條件)。在同時獲得lock1 和 lock2后,線程就打印出標志著任務(wù)完成的信息“My Job done”。
執(zhí)行結(jié)果:
13:My Job done
14:My Job done
公平鎖
在大多數(shù)情況下,所得申請都是非公平的。也就是說,線程1首先請求了鎖A,接著線程2也請求了鎖A。那么當鎖A可用時,是線程1可以獲得鎖還是線程2可以獲得鎖呢?這個是不一定的。系統(tǒng)只是會從這個鎖的等待隊列中隨機的挑選一個。因此不能保證其公平性。
而公平的鎖則不會是這樣的,它會按照時間的先后順序,保證先到者先得,后到者后得。公平鎖的一大特點是:它不會產(chǎn)生饑餓現(xiàn)象。只要你排隊,最終還是可以等到資源的。如果我們使用synchronized關(guān)鍵字進行鎖控制,那么產(chǎn)生的鎖就是非公平的。而重入鎖允許我們對其公平性進行設(shè)置。它有一個如下的構(gòu)造函數(shù):
public ReentrantLock (boolean fair)
當參數(shù)fair為true時,表示鎖時公平的。公平鎖看起來很優(yōu)美,但是要實現(xiàn)公平鎖必然要求系統(tǒng)維護一個有序隊列,因此公平鎖的實現(xiàn)成本比較高,性能相對也非常低下,因此,默認情況下,鎖時非公平的。如果沒有特別的需求,也不需要使用公平鎖。公平鎖和非公平鎖在線程調(diào)度上也是又極大差別的。下面代碼可以很好的突出公平鎖的特點:
/**
* @Author: 王琦 <QQ.Eamil>1124602935@qq.com</QQ.Eamil>
* @Date: 2019-5-4 0004 0:15
* @Description: 重入鎖之公平鎖
*/
public class FairLock implements Runnable {
// 創(chuàng)建一個公平鎖:fairLock
public static ReentrantLock fairLock = new ReentrantLock(true);
@Override
public void run() {
while (true){
try {
fairLock.lock();
System.out.println(Thread.currentThread().getName() + "獲得鎖");
} finally {
fairLock.unlock();
}
}
}
public static void main(String[] args) {
FairLock fairLock = new FairLock();
Thread t1 = new Thread(fairLock, "Thread_t1");
Thread t2 = new Thread(fairLock, "Thread_t2");
t1.start();
t2.start();
}
}
代碼很簡單,我們直接看運行結(jié)果:
從結(jié)果可以很明顯的看出兩個線程基本上是交替獲得鎖的,幾乎不會發(fā)生一個線程連續(xù)多此獲得鎖的可能,從而公平性也得到了保障。如果不使用公平鎖,那么情況就完全不一樣了:
可以看出來根據(jù)系統(tǒng)的調(diào)度,一個線程會傾向于再次獲取已經(jīng)持有的鎖,這種分配方式是高效的,但是無公平性可言。
總結(jié)
一、ReentrantLock的幾個重要方法整理如下:
(1) lock():獲得鎖,如果鎖已經(jīng)被占用,則等待。
(2) lockInterruptibly():獲得鎖,但優(yōu)先響應(yīng)中斷。
(3) tryLock():嘗試獲得鎖,如果成功,返回true,失敗返回false。該方法不等待,立即返回。
(4) tryLock(long time, TimeUnit unit):在給定時間內(nèi)嘗試獲得鎖。
(5) unlock():釋放鎖。
二、就重入鎖的實現(xiàn)來看,它主要幾種在Java層面。在重入鎖的實現(xiàn)中,主要包含三個要素:
(1) 原子狀態(tài)。原子狀態(tài)使用CAS操作來存儲當前鎖的狀態(tài),判斷鎖是否已經(jīng)被別的線程持有。
(2) 等待隊列。所有沒有請求到鎖的線程,會進入等待隊列進行等待。持有線程釋放鎖后,系統(tǒng)就能從等待隊列中喚醒一個線程,繼續(xù)工作。
(3) 阻塞原語park()和unpark(),用來掛起和回復(fù)線程。
三、延申:重入鎖的好搭檔:Condition條件,這里就不說了,感興趣的同學(xué)自己去查一下。