一、ReentrantLock重入鎖
1、ReentrantLock重入鎖簡(jiǎn)介
ReentrantLock可以完全替代synchronized關(guān)鍵字。在JDK5.0的早期版本中,ReentrantLock的性能遠(yuǎn)遠(yuǎn)好于synchronized,但是兇JDK6.0開始synchronized上做了大量?jī)?yōu)化,使得兩者性能差距并不大。
重入鎖使用java.until.concurrent.locks.ReentrantLock類來實(shí)現(xiàn)。ReenterLock.java展示了簡(jiǎn)單的.ReentrantLock使用案例。
public class ReenterLock implements Runnable
{
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run()
{
for (int j = 0; j < 10000000; ++j)
{
lock.lock(); //對(duì)臨界區(qū)加鎖
try
{
++i;
}
finally
{
lock.unlock(); //退出臨界區(qū)時(shí)必須釋放鎖
}
}
}
public static void main(String[] args) throws InterruptedException
{
Thread t1 = new Thread(new ReenterLock());
Thread t2 = new Thread(new ReenterLock());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
從上面的代碼可以看到,與synchronized相比,ReentrantLock有著顯式的操作過程。開發(fā)人員必須手動(dòng)指定何時(shí)加鎖,何時(shí)釋放鎖。因此ReentrantLock對(duì)邏輯控制的靈活性遠(yuǎn)好于synchronized。但是必須注意,在退出臨界區(qū)時(shí),一定要釋放鎖。否則其他線程就沒機(jī)會(huì)再訪問臨界區(qū)了。
ReentrantLock之所以叫重入鎖,是因?yàn)檫@種鎖是可以反復(fù)進(jìn)入的。當(dāng)然,這里的反復(fù)僅僅局限于一個(gè)線程。
如下面代碼展示:
lock.lock(); //對(duì)臨界區(qū)加鎖
lock.lock();
try
{
++i;
}
finally
{
lock.unlock(); //退出臨界區(qū)時(shí)必須釋放鎖
lock.unlock();
}
在這種情況下,一個(gè)線程連續(xù)兩次獲得通一把鎖,這是允許的,如果不允許的話,那么第二個(gè)線程在第2次獲得鎖時(shí),將會(huì)和自己產(chǎn)生死鎖。但是值得注意的是,如果一個(gè)線程多次獲得鎖,那么在釋放鎖的時(shí)候,也必須釋放相同的次數(shù),如果釋放鎖的次數(shù)多,那么就會(huì)得到一個(gè)java.lang.IllegalMonitorStateException異常,反之,如果釋放的鎖次數(shù)少了,那么相當(dāng)于線程還持有這個(gè)鎖,其他線程也無法進(jìn)入臨界區(qū)。
2、中斷響應(yīng)
當(dāng)使用synchronized來對(duì)臨界區(qū)加鎖,如果一個(gè)線程在等待鎖,那么結(jié)果只有兩種情況,要么它獲得鎖繼續(xù)執(zhí)行,要么它就保持等待。但是如果使用重入鎖,就會(huì)提供另一種選擇,那就是線程可以被中斷。也就是說在等待鎖的過程中,程序可以根據(jù)需要取消對(duì)鎖的請(qǐng)求。
下面的代碼IntLock.java會(huì)產(chǎn)生一個(gè)死鎖,但是得益于鎖中斷,我們可以很輕易的解決這個(gè)死鎖。
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();
try
{
Thread.sleep(500);
}
catch (InterruptedException e) {}
lock2.lockInterruptibly();
}
else
{
lock2.lockInterruptibly();
try
{
Thread.sleep(500);
}
catch (InterruptedException e) {}
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 r1 = new IntLock(1);
IntLock r2 = new IntLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
Thread.sleep(1000);
//中斷其中一個(gè)線程
t2.interrupt();
}
}
線程t1和t2啟動(dòng)后,t1先占用lock1,再占用lock2;t2先占用lock2,再占用lock1。因此很容易形成t1和t2之間的相互等待。在這里對(duì)鎖的請(qǐng)求,統(tǒng)一使用lockInterruptibly()方法。這是一個(gè)可以對(duì)中斷進(jìn)行響應(yīng)的鎖申請(qǐng)動(dòng)作,即在等待鎖的過程中,可以響應(yīng)中斷。
當(dāng)代碼執(zhí)行到Thread.sleep(1000)時(shí),主線程處于休眠,此時(shí),這兩個(gè)線程處于死鎖的狀態(tài),當(dāng)執(zhí)行到t2.interrupt()時(shí),由于t2被中斷,故t2會(huì)放棄對(duì)lock1的申請(qǐng),同時(shí)釋放已獲得的lock2,這個(gè)操作導(dǎo)致t1線程可以順利得到lock2而繼續(xù)執(zhí)行下去。
執(zhí)行上述代碼,可以看到以下輸出:

可以看到,中斷后,兩個(gè)線程雙雙退出。但真正完成工作的只有t1,而t2線程則放棄其任務(wù)直接退出,釋放資源。
3、鎖申請(qǐng)等待限時(shí)
除了等待外部通知之外,要避免死鎖還有另外一種方法,那就是限時(shí)等待。對(duì)于線程來說,通常我們無法判斷為什么一個(gè)線程遲遲拿不到鎖。也許是因?yàn)殒i死了,也許因?yàn)楫a(chǎn)生了饑餓。但如果給定一個(gè)等待時(shí)間,讓線程自動(dòng)放棄,那么對(duì)系統(tǒng)來說是有意義的。我們可以使用tryLock()方法進(jìn)行一次限時(shí)等待。
下面代碼TimeLock.java展示了限時(shí)等待鎖的使用
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 tl = new TimeLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
}
}
在這里,tryLock()方法接收兩個(gè)參數(shù),一個(gè)表示等待時(shí)長(zhǎng),另外一個(gè)表示計(jì)時(shí)單位。這里的單位設(shè)置為秒,時(shí)長(zhǎng)是5,表示線程在這個(gè)鎖請(qǐng)求中,最多等待5秒。如果超過五秒還沒有得到鎖,就會(huì)返回false。如果成功獲得鎖,則返回true。
在本例中,由于占用鎖的線程會(huì)持有鎖長(zhǎng)達(dá)6秒,故另一個(gè)線程無法再5秒的等待時(shí)間內(nèi)獲得鎖,因此,請(qǐng)求會(huì)失敗。
ReentrantLock.tryLock()方法也可以不帶參數(shù)直接運(yùn)行。在這種情況下,當(dāng)前線程會(huì)嘗試獲得鎖,如果鎖并未被其他線程占用,則申請(qǐng)鎖會(huì)成功,并立即返回true。如果鎖被其他線程占用,則當(dāng)前線程不會(huì)進(jìn)行等待,而是立即返回false。這種方式不會(huì)引起線程等待,因此不會(huì)產(chǎn)生死鎖。
4、公平鎖
大多數(shù)情況下,鎖的申請(qǐng)都是非公平的。舉個(gè)例子,線程1首先請(qǐng)求了鎖A,接著線程2也請(qǐng)求了鎖A。那么當(dāng)鎖A可用時(shí),是線程1可以獲得鎖還是線程2可以獲得鎖呢?這是不一定的。系統(tǒng)只是會(huì)從這個(gè)鎖的等待隊(duì)列中隨機(jī)挑選一個(gè),因此不能保證其公平性。而公平的鎖,則不是這樣,它會(huì)按照時(shí)間的先后順序,保證先到者先得,后到者后得。公平鎖的一大特點(diǎn)是:它不會(huì)產(chǎn)生饑餓現(xiàn)象。只要你排隊(duì),最終還是可以等到資源的。如果我們使用synchronized關(guān)鍵字進(jìn)行鎖控制,那么產(chǎn)生的鎖就是非公平的。而重入鎖允許我們對(duì)其公平性進(jìn)行設(shè)置。它有一個(gè)如下的構(gòu)造函數(shù):
public ReentrantLock(boolean fair)
當(dāng)參數(shù)fair為true時(shí),表示鎖時(shí)公平的。但是實(shí)現(xiàn)公平鎖必須要犧牲一定的性能,因?yàn)樾枰S護(hù)一個(gè)有序的隊(duì)列,因此默認(rèn)情況下,鎖是非公平的。如果沒有特別的要求,也不需要使用公平鎖。下面的代碼可以很好的突出公平鎖的特點(diǎn):
public class FairLock implements Runnable
{
//當(dāng)參數(shù)fair為true時(shí),表示鎖是公平的。默認(rèn)為false,非公平
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 r1 = new FairLock();
Thread t1 = new Thread(r1, "Thread_t1");
Thread t2 = new Thread(r1, "Thread_t2");
t1.start();
t2.start();
}
}
執(zhí)行代碼,會(huì)看到如下輸出結(jié)果:

因?yàn)榇a會(huì)產(chǎn)生大量輸出,這里只截取部分進(jìn)行說明。在這個(gè)輸出中,很明顯可以看到,兩個(gè)線程基本上是交替獲得鎖的,幾乎不會(huì)發(fā)生一個(gè)線程連續(xù)多次獲得鎖的可能,從而公平性也得到了保證。但是如果使用不公平鎖,那么情況就會(huì)不一樣,部分輸出如下:

可以看到,根據(jù)系統(tǒng)的調(diào)度,一個(gè)線程會(huì)傾向于再次獲取已經(jīng)持有的鎖,這種分配方式是高效的,但是無公平性可言。
5、總結(jié)
(1)ReentrantLock 幾個(gè)重要方法
- lock():獲得鎖,如果已經(jīng)被占用,則等待。
- lockInterruptibly():獲得鎖,但優(yōu)先響應(yīng)中斷。
- tryLock():嘗試獲得鎖,如果成功,返回true,失敗返回false。該方法不等待,立即返回。
- tryLock(long time, TimeUnit unit):在給定時(shí)間內(nèi)嘗試獲得鎖。
- unlock():釋放鎖。
(2)重入鎖實(shí)現(xiàn)中包含的三要素
- 原子狀態(tài):原子狀態(tài)使用CAS操作來存儲(chǔ)當(dāng)前鎖的狀態(tài),判斷鎖是否已經(jīng)被別的線程持有。
- 等待隊(duì)列:所有沒有請(qǐng)求到鎖的線程,會(huì)進(jìn)入等待隊(duì)列進(jìn)行等待,待有線程釋放鎖后,系統(tǒng)就能從等待隊(duì)列中喚醒一個(gè)線程,繼續(xù)工作。
- 阻塞原語park()和unpark(),用來掛起和恢復(fù)線程。沒有得到鎖的線程會(huì)被掛起。