1、ReentrantLock特性
??ReentrantLock是一把重入鎖脖含,可中斷豁翎,可以限時角骤,支持公平鎖和非公平鎖。
??下面舉一個生活中的例子心剥,幫助大家來更好的理解ReentrantLock這些特性邦尊。
??快過年了,在北上廣的小伙伴們紛紛踏上了回家的旅途优烧。由于小伙伴們一年都沒回家了蝉揍,一下班拿起行李箱就奔向了火車站。爭先恐后的跑到G1020檢票口檢票回家畦娄,誰先跑到檢票口又沾,誰先驗票,誰先回家熙卡。那些跑的慢沒有搶到檢票權(quán)的杖刷,一個一個的在后面排成一隊,先到的排在前面驳癌。在最前面獲得檢票權(quán)的人通過檢票機驗票走人,檢票機通知下面一個人來驗票滑燃。這樣每次最前面的人驗票走人,然后依次下一個颓鲜。這樣一來大家都井然有序的回家表窘,心里面也比較平衡典予,誰讓自己腿短跑的慢呢,排在跑的快的人后面也心安理得乐严。這就是公平鎖的基本思路熙参。
??在看G1028檢票口的人,這批人就比較聰明了麦备,看著前面一眼望不到頭的長長的隊伍孽椰。把自己的行李箱放在隊伍中代替自己,自己則在旁邊的椅子上歇著凛篙。沒辦法誰讓做這趟車的人聰明呢黍匾,這下可把檢票機給累壞了,每次檢查到行李箱到就大聲喊呛梆,黑色行李箱的來安檢了锐涯,然后聽到的人慢悠悠的在過去驗票走人。后來智能的檢票機通過智能學(xué)習(xí)也變聰明了填物,如果在檢查到放行李箱占位置的纹腌,如果此時剛好有人過來檢票,則直接讓此人檢票滞磺,不需要在隊伍尾部排隊等候了升薯。這樣雖然對搶到前面的人不公平夏跷,但是卻加快了進站效率规伐。這就是非公平鎖的基本思路。
??現(xiàn)在檢票的規(guī)則變了典鸡,以家庭為單位檢票阅茶,只要是家庭中的一員搶到檢票口蛛枚,其余人就可以跟著過檢票口。由于小明跑的快脸哀,先跑到了檢票口蹦浦。他的父母年齡比較大,跑的比較慢撞蜂。等跑到檢票口的時候盲镶,后面已經(jīng)排起來了長隊×律悖可是人家有一個跑的快的兒子呀徒河,現(xiàn)在的規(guī)則又是以家庭為單位,都是一家人送漠,于是小明的父母也可以直接檢票走人顽照,不需要去隊伍里面排隊。這就是可重入鎖,同一個線程可以重復(fù)拿鎖代兵。
??快看G1028檢票口的人吵起來了尼酿,怎么回事呢。原來是禿頭和長毛兩個人剛才跑的快撞在了一起植影,身份ID都掉在了地上裳擎,2個人匆忙撿起來就跑到了檢票口。禿頭跑的快一點思币,跑到了長毛的前面鹿响。這時候禿頭拿身份id驗證的時候發(fā)現(xiàn)身份不對,上面寫的是產(chǎn)品經(jīng)理谷饿。后面那個人拿的是禿頭的身份ID惶我,上面寫的程序員。于是禿頭說,我把你的身份ID給你博投,你把我的身份ID給我绸贡。但是長毛平時提需求提的習(xí)慣了,對禿頭說給你可以毅哗,但是讓我排到你前面听怕。禿頭一聽這無腦需求,火冒三丈虑绵,就和長毛干了起來尿瞭。這下好了,長毛不給禿頭身份ID蒸殿,禿頭也不給長毛身份ID筷厘,兩個人就互相僵持著鸣峭。這導(dǎo)致后面排隊的人也沒發(fā)進站了宏所。這時候長毛手機發(fā)生了異常,一個電話打了過來摊溶,原來老板讓長毛回去改需求爬骤,沒辦法最后中斷了2人的爭執(zhí)。長毛灰溜溜的走了莫换,禿頭打贏了這場仗霞玄,臉上露出了陽光般的笑容。這就是可中斷的拉岁,當(dāng)2個線程互相占有鎖坷剧,不釋放導(dǎo)致死鎖的時候,ReentrantLock可以用鎖中斷解決喊暖。
??在看另一邊一個人想要插隊惫企,在耐心的說服前面的人,讓我先進去吧,讓我先進去吧狞尔〈园妫可是說服了幾分鐘也沒說服成功,于是放棄了不在插隊搶先檢票了偏序。這個就是在一定時間內(nèi)鎖嘗試,嘗試著去獲取鎖页畦,如果沒有獲取到就結(jié)束。
??以上就是重入鎖研儒,鎖中斷豫缨,鎖限時,公平鎖和非公平鎖的大致概念端朵,相信大家應(yīng)該會有所理解州胳。下面對ReentrantLock的特性進行詳細的講解。
2逸月、ReentrantLock非公平鎖
ReentrantLock默認實現(xiàn)的是非公平鎖栓撞,我具體看一下代碼:
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
我們看一下構(gòu)造函數(shù):
public ReentrantLock() {
sync = new NonfairSync();
}
從這里可以看出ReentrantLock默認實現(xiàn)的是非公平鎖,我們在看一下非公平鎖是怎么具體實現(xiàn)的碗硬。
lock是一個接口,構(gòu)造器默認實現(xiàn)的是NonfairSync瓤湘,所reentrantLock.lock() 調(diào)用的是NonfairSync的lock接口。
看一下這塊的具體代碼:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
??NonfairSync 繼承了Sync,Sync繼承了AbstractQueuedSynchronizer恩尾,到這里大家應(yīng)該已經(jīng)明白ReentrantLock是基于AQS實現(xiàn)的弛说,所以只要你搞懂AQS,很多并發(fā)類你都會很容易的理解翰意。
??reentrantLock.lock()木人,我們看一下lock方法做了哪些操作,首先通過CAS獲取鎖冀偶,如果獲取到鎖醒第,把當(dāng)前線程設(shè)置為獨占線程。如果獲取失敗进鸠,則調(diào)用acquire方法稠曼,而此方法為AQS內(nèi)部方法,此處不在詳細的展開分析客年。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
??這里有一個知識點霞幅,我們看一下NonfairSync類中tryAcquire方法,此方法也是AQS中的方法量瓜,也就是子類NonfairSync重寫了父類AQS的tryAcquire方法司恳。
??當(dāng)子類NonfairSync調(diào)用acquire方法的時候,執(zhí)行的是AQS提供的acquire方法绍傲,然后從上面代碼中可以看出來父類AQS在此方法中執(zhí)行了tryAcquire方法扔傅,而tryAcquire方法在子類中已經(jīng)重寫,那么就會執(zhí)行子類NonfairSync實現(xiàn)的tryAcquire方法。這就是多態(tài)铅鲤。
??這也是AQS的好處划提,對外提供API,子類繼承AQS邢享,按照自己的業(yè)務(wù)邏輯重寫提供的API鹏往。而AQS只管線程怎么進行入隊列,怎么插入節(jié)點骇塘,怎么喚醒節(jié)點這些底層的方法伊履,對外層提供調(diào)用的 API,然后子類只需要繼承AQS款违,實現(xiàn)獨有的業(yè)務(wù)方法即可唐瀑,從而大大降低了耦合度。
我們在看一下nonfairTryAcquire方法做了哪寫操作:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//獲取當(dāng)前線程
int c = getState();//獲取當(dāng)前線程的狀態(tài)
if (c == 0) {//如果當(dāng)前線程處于初始狀態(tài)
if (compareAndSetState(0, acquires)) {//cas競爭鎖
setExclusiveOwnerThread(current);//競爭到鎖把當(dāng)前線程設(shè)置為獨占線程
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//如果當(dāng)前線程是獨占線程插爹,注意此方法也是實現(xiàn)重入的地方哄辣。
int nextc = c + acquires;//當(dāng)前線程狀態(tài)值+傳入的值
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//設(shè)置最新的狀態(tài)值
return true;
}
return false;
}
??首先取到當(dāng)前線程和當(dāng)前線程的值,如果當(dāng)前線程是初始狀態(tài)那么就去競爭鎖赠尾,如果競爭到鎖力穗,把當(dāng)前線程設(shè)置為獨占線程。如果進來的線程是獨占線程气嫁,那么更新此線程進入的次數(shù)当窗,同時也可以獲取鎖。如果沒有競爭到鎖寸宵,也不是當(dāng)前的獨占線程崖面,那么就返回false。
??從此方法中可以看出來梯影,只要有線程進來巫员,就讓他獲取鎖,而不是排隊到尾部光酣。只要是獨占線程疏遏,就可以重復(fù)進來,正是通過此方法可以看出來ReentrantLock是可以進行重入的也是可以是實現(xiàn)非公平鎖的救军。
3、ReentrantLock公平鎖
我們在看一下ReentrantLock實現(xiàn)的公平鎖的源代碼:
ReentrantLock reentrantLock=new ReentrantLock(true)倘零;
reentrantLock.lock();
我們點擊構(gòu)造器看一下:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
下面具體看一下FairSync類的源代碼:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//沒有線程在等隊列里面等待同時獲取到鎖唱遭,則設(shè)置當(dāng)前線程為獨占線程
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
??FairSync也是繼承了Sync,也就是也是繼承了AQS呈驶,調(diào)用lock方法拷泽,lock方法調(diào)用了父類acquire,此方法會調(diào)用子類重寫的tryAcquire方法。它的實現(xiàn)方式和上面講的非公平鎖實現(xiàn)方式大致一樣司致,業(yè)務(wù)邏輯都是在重寫的tryAcquire里面拆吆。
我們看一下hasQueuedPredecessors:
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
此方法主要是查看是否有線程在等待隊列里面等待。
??FairSync類tryAcquire方法業(yè)務(wù)邏輯就是獲取到當(dāng)前線程和當(dāng)前線程的狀態(tài)脂矫,如果當(dāng)前線程是初始狀態(tài)枣耀,會去判斷當(dāng)前隊列里面是否有等待的線程,如果隊列中沒有等待的線程庭再,同時獲取到鎖捞奕,那么就把當(dāng)前線程設(shè)置為獨占線程。如果是相同的獨占線程進來拄轻,則更新獨占線程進來的次數(shù)颅围。同時返回true,否則返回false恨搓。從這里可以很容易的看出來院促,這是一個公平鎖,進來的線程需要排隊斧抱,隊列中沒有了線程才能輪到進來的線程一疯。同時在else if 這個條件中可以看出來也是可重入的。
3夺姑、ReentrantLock鎖中斷
我們看一下可中斷鎖的源代碼:
ReentrantLock reentrantLock=new ReentrantLock();
reentrantLock.lockInterruptibly();
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);//調(diào)用AQS中方法
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())//線程中斷拋出異常
throw new InterruptedException();
if (!tryAcquire(arg))//此處還是調(diào)用創(chuàng)建對象子類的方法,獲取不到鎖執(zhí)行下面的方法
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);//封裝線程節(jié)點,并且添加到尾部墩邀。前面文章已經(jīng)詳細講解過,此處不在詳細展開盏浙。
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();//獲取當(dāng)前節(jié)點的前驅(qū)節(jié)點
if (p == head && tryAcquire(arg)) {//前驅(qū)節(jié)點是頭節(jié)點并且獲取到鎖
setHead(node);//設(shè)置當(dāng)前節(jié)點為頭節(jié)點
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//線程如果是阻塞的并且被中斷眉睹,則直接拋出異常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);//如果線程拋出了異常,那么就把線程狀態(tài)設(shè)置為取消狀態(tài)同時清除節(jié)點.
}
}
??此處講解一下废膘,為什么ReentrantLock可以進行鎖中斷竹海,為什么可以在產(chǎn)生死鎖的時候,可以通過鎖中斷技術(shù)解決死鎖丐黄≌洌看過源碼其實已經(jīng)明白,首先在當(dāng)前線程如果調(diào)用了interrupted灌闺,那么直接拋出異常出來艰争。如果線程被阻塞并且被中斷了那么也是拋出異常。也就是他是通過線程調(diào)用中斷方法拋出異常來打破持有鎖的桂对。如果前面的文章看過甩卓,你會發(fā)在AQS中doAcquireInterruptibly方法和acquireQueued方法很相似,區(qū)別就是一個是返回boolean類型的值蕉斜,讓上層做判斷逾柿,一個是在返回boolean類型值的地方直接拋出了異常缀棍。
3、ReentrantLock鎖限時
我們看一下可中斷鎖的源代碼:
ReentrantLock reentrantLock=new ReentrantLock();
reentrantLock.tryLock(300, TimeUnit.SECONDS);//300秒內(nèi)持續(xù)獲取鎖,直到獲取到鎖或者時間截止
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())//此處可以看出來支持鎖中斷
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);//首先獲取一次鎖机错,如果沒有獲取到執(zhí)行獨占計時模式
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)//時間小于0直接返回
return false;
final long deadline = System.nanoTime() + nanosTimeout;//隊列延遲時間為系統(tǒng)時間+設(shè)置的超時時間
final Node node = addWaiter(Node.EXCLUSIVE);//把當(dāng)前線程封裝為Node并添加到隊列
boolean failed = true;
try {
for (;;) {//自旋
final Node p = node.predecessor();//獲取當(dāng)前節(jié)點的前驅(qū)節(jié)點
if (p == head && tryAcquire(arg)) {//如果前驅(qū)節(jié)點是頭節(jié)點并且獲取到鎖
setHead(node);//設(shè)置當(dāng)前節(jié)點為頭節(jié)點
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();//超時間為延遲時間-當(dāng)前系統(tǒng)的時間
if (nanosTimeout <= 0L)//表示已經(jīng)超過設(shè)置的嘗試時間爬范,直接返回
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)//如果當(dāng)前線程阻塞,并且超時時間大于1000納秒
LockSupport.parkNanos(this, nanosTimeout);//阻塞當(dāng)前線程并在超時時間內(nèi)返回
if (Thread.interrupted())//線程中斷
throw new InterruptedException();//拋出異常
}
} finally {
if (failed)//線程發(fā)生異常
cancelAcquire(node);//把當(dāng)前線程設(shè)置為取消狀態(tài)并清除該節(jié)點
}
}
通過閱讀源碼我們發(fā)現(xiàn)鎖限時獲取的步驟:
1 首先調(diào)用tryAcquire方法獲取一次鎖,如果沒有獲取到調(diào)用AQS中的doAcquireNanos弱匪。
2 System.nanoTime() 獲取系統(tǒng)納秒級時間+傳遞的延時時間為最后的時間青瀑。
3 調(diào)用addWaiter方法把當(dāng)前線程封裝為節(jié)點并添加到隊列的尾部。
4 前驅(qū)節(jié)點是頭節(jié)點并且獲取到鎖設(shè)置當(dāng)前節(jié)點為頭節(jié)點
5 如果當(dāng)前線程被阻塞了并且超時時間大于1000納秒痢法,調(diào)用LockSupport.parkNanos方法阻塞當(dāng)前線程并且在規(guī)定的超時間內(nèi)返回
6 如果線程中斷狱窘,直接拋出異常,這里可以看出支持鎖中斷
7 如果線程在自旋的過程中發(fā)生了異常财搁,那么調(diào)用cancelAcquire方法把當(dāng)前線程設(shè)置為取消狀態(tài)并且清除該節(jié)點蘸炸。
??其實此方法和上一篇講解的獨占鎖模式調(diào)用acquireQueued方法差不多。不同點在于這里增加了超時時間尖奔,如果超時時間大于spinForTimeoutThreshold搭儒,此值是一個常量為1000的值。也就是如果超時時間大于1000納秒提茁,那么就調(diào)用 LockSupport.parkNanos方法讓該線程阻塞淹禾,最長阻塞的時間不會超過超時的時間。同時增加了線程中斷的判斷茴扁,發(fā)生線程中斷則拋出異常铃岔,其余和acquireQueued實現(xiàn)都一樣。