6.Lock.lock

Lock.lock/unlock

接上篇,這篇從Lock.lock/unlock開始葵袭。特別說明在沒有特殊情況下所有程序尸饺、API、文檔都是基于JDK 6.0的沈堡。

public void java.util.concurrent.locks.ReentrantLock.lock()

獲取鎖静陈。

如果該鎖沒有被另一個線程保持,則獲取該鎖并立即返回诞丽,將鎖的保持計數(shù)設(shè)置為 1鲸拥。

如果當前線程已經(jīng)保持該鎖,則將保持計數(shù)加 1僧免,并且該方法立即返回刑赶。

如果該鎖被另一個線程保持,則出于線程調(diào)度的目的懂衩,禁用當前線程撞叨,并且在獲得鎖之前,該線程將一直處于休眠狀態(tài)浊洞,此時鎖保持計數(shù)被設(shè)置為 1牵敷。

從上面的文檔可以看出ReentrantLock是可重入鎖的實現(xiàn)。而內(nèi)部是委托java.util.concurrent.locks.ReentrantLock.Sync.lock()實現(xiàn)的法希。java.util.concurrent.locks.ReentrantLock.Sync是抽象類枷餐,有java.util.concurrent.locks.ReentrantLock.FairSync和java.util.concurrent.locks.ReentrantLock.NonfairSync兩個實現(xiàn),也就是常說的公平鎖和不公平鎖苫亦。

公平鎖和非公平鎖

如果獲取一個鎖是按照請求的順序得到的毛肋,那么就是公平鎖奕锌,否則就是非公平鎖。

在沒有深入了解內(nèi)部機制及實現(xiàn)之前村生,先了解下為什么會存在公平鎖和非公平鎖惊暴。公平鎖保證一個阻塞的線程最終能夠獲得鎖,因為是有序的趁桃,所以總是可以按照請求的順序獲得鎖辽话。不公平鎖意味著后請求鎖的線程可能在其前面排列的休眠線程恢復前拿到鎖,這樣就有可能提高并發(fā)的性能卫病。這是因為通常情況下掛起的線程重新開始與它真正開始運行油啤,二者之間會產(chǎn)生嚴重的延時。因此非公平鎖就可以利用這段時間完成操作蟀苛。這是非公平鎖在某些時候比公平鎖性能要好的原因之一益咬。

二者在實現(xiàn)上的區(qū)別會在后面介紹,我們先從公平鎖(FairSync)開始帜平。

前面說過java.util.concurrent.locks.AbstractQueuedSynchronizer (AQS)是Lock的基礎(chǔ)幽告,對于一個FairSync而言,lock()就直接調(diào)用AQS的acquire(int arg);

public final void acquire(int arg) 以獨占模式獲取對象裆甩,忽略中斷冗锁。通過至少調(diào)用一次 *tryAcquire(int)* 來實現(xiàn)此方法,并在成功時返回嗤栓。否則在成功之前冻河,一直調(diào)用 *tryAcquire(int)* 將線程加入隊列,線程可能重復被阻塞或不被阻塞茉帅。

在介紹實現(xiàn)之前先要補充上一節(jié)的知識叨叙,對于一個AQS的實現(xiàn)而言,通常情況下需要實現(xiàn)以下方法來描述如何鎖定線程堪澎。

**tryAcquire(int)**試圖在獨占模式下獲取對象狀態(tài)擂错。此方法應該查詢是否允許它在獨占模式下獲取對象狀態(tài),如果允許全封,則獲取它马昙。此方法總是由執(zhí)行 acquire 的線程來調(diào)用。如果此方法報告失敗刹悴,則 acquire 方法可以將線程加入隊列(如果還沒有將它加入隊列),直到獲得其他某個線程釋放了該線程的信號攒暇。也就是說此方法是一種嘗試性方法土匀,如果成功獲取鎖那最好,如果沒有成功也沒有關(guān)系形用,直接返回false就轧。

**tryRelease(int)**試圖設(shè)置狀態(tài)來反映獨占模式下的一個釋放证杭。 此方法總是由正在執(zhí)行釋放的線程調(diào)用。釋放鎖可能失敗或者拋出異常妒御,這個在后面會具體分析解愤。

**tryAcquireShared(int)** 試圖在共享模式下獲取對象狀態(tài)。

**tryReleaseShared(int)** 試圖設(shè)置狀態(tài)來反映共享模式下的一個釋放乎莉。

**isHeldExclusively()** 如果對于當前(正調(diào)用的)線程送讲,同步是以獨占方式進行的,則返回 true惋啃。

除了tryAcquire(int)外哼鬓,其它方法會在后面具體介紹。首先對于ReentrantLock而言边灭,不管是公平鎖還是非公平鎖异希,都是獨占鎖,也就是說同時能夠有一個線程持有鎖绒瘦。因此對于acquire(int arg)而言称簿,arg==1。在AQS中acquire的實現(xiàn)如下:

public final void acquire(int arg) {
? if (!tryAcquire(arg) &&
? acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
? selfInterrupt();
}

這個看起來比較復雜惰帽,我們分解以下4個步驟予跌。

    1. 如果tryAcquire(arg)成功,那就沒有問題善茎,已經(jīng)拿到鎖券册,整個lock()過程就結(jié)束了。如果失敗進行操作2垂涯。
    2. 創(chuàng)建一個獨占節(jié)點(Node)并且此節(jié)點加入CHL隊列末尾烁焙。進行操作3。
    3. 自旋嘗試獲取鎖耕赘,失敗根據(jù)前一個節(jié)點來決定是否掛起(park())骄蝇,直到成功獲取到鎖。進行操作4操骡。
    4. 如果當前線程已經(jīng)中斷過九火,那么就中斷當前線程(清除中斷位)。

這是一個比較復雜的過程册招,我們按部就班一個一個分析岔激。

tryAcquire(acquires)

對于公平鎖而言,它的實現(xiàn)方式如下:

? protected final boolean tryAcquire(int acquires) {
? final Thread current = Thread.currentThread();
? int c = getState();
? if (c == 0) {
? if (isFirst(current) &&
? 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;
? }
}

在這段代碼中是掰,前面說明對于AQS存在一個state來描述當前有多少線程持有鎖虑鼎。由于AQS支持共享鎖(例如讀寫鎖,后面會繼續(xù)講),所以這里state>=0炫彩,但是由于ReentrantLock是獨占鎖匾七,所以這里不妨理解為0<=state,acquires=1江兢。isFirst(current)是一個很復雜的邏輯昨忆,包括踢出無用的節(jié)點等復雜過程,這里暫且不提杉允,大體上的意思是說判斷AQS是否為空或者當前線程是否在隊列頭(為了區(qū)分公平與非公平鎖)邑贴。

    1. 如果當前鎖有其它線程持有,c!=0夺颤,進行操作2痢缎。否則,如果當前線程在AQS隊列頭部世澜,則嘗試將AQS狀態(tài)state設(shè)為acquires(等于1)独旷,成功后將AQS獨占線程設(shè)為當前線程返回true,否則進行2寥裂。這里可以看到compareAndSetState就是使用了CAS操作嵌洼。
    2. 判斷當前線程與AQS的獨占線程是否相同,如果相同封恰,那么就將當前狀態(tài)位加1(這里+1后結(jié)果為負數(shù)后面會講麻养,這里暫且不理它),修改狀態(tài)位诺舔,返回true鳖昌,否則進行3。這里之所以不是將當前狀態(tài)位設(shè)置為1低飒,而是修改為舊值+1呢许昨?這是因為ReentrantLock是可重入鎖,同一個線程每持有一次就+1褥赊。
    3. 返回false糕档。

比較非公平鎖的tryAcquire實現(xiàn)java.util.concurrent.locks.ReentrantLock.Sync.nonfairTryAcquire(int),公平鎖多了一個判斷當前節(jié)點是否在隊列頭拌喉,這個就保證了是否按照請求鎖的順序來決定獲取鎖的順序(同一個線程的多次獲取鎖除外)速那。

現(xiàn)在再回頭看公平鎖和非公平鎖的lock()方法。公平鎖只有一句acquire(1)尿背;而非公平鎖的調(diào)用如下:

final void lock() {
? if (compareAndSetState(0, 1))
? setExclusiveOwnerThread(Thread.currentThread());
? else
? acquire(1);
}

很顯然端仰,非公平鎖在第一次獲取鎖,或者其它線程釋放鎖后(可能等待)残家,優(yōu)先采用compareAndSetState(0,1)然后設(shè)置AQS獨占線程而持有鎖榆俺,這樣有時候比acquire(1)順序檢查鎖持有而要高效。即使在重入鎖上坞淮,也就是compareAndSetState(0,1)失敗茴晋,但是是當前線程持有鎖上,非公平鎖也沒有問題回窘。

addWaiter(mode)

tryAcquire失敗就意味著入隊列了诺擅。此時AQS的隊列中節(jié)點Node就開始發(fā)揮作用了。一般情況下AQS支持獨占鎖和共享鎖啡直,而獨占鎖在Node中就意味著條件(Condition)隊列為空(上一篇中介紹過相關(guān)概念)烁涌。在java.util.concurrent.locks.AbstractQueuedSynchronizer.Node中有兩個常量,

static final Node EXCLUSIVE = null; //獨占節(jié)點模式

static final Node SHARED = new Node(); //共享節(jié)點模式

addWaiter(mode)中的mode就是節(jié)點模式酒觅,也就是共享鎖還是獨占鎖模式撮执。

前面一再強調(diào)ReentrantLock是獨占鎖模式。

private Node addWaiter(Node mode) {
? Node node = new Node(Thread.currentThread(), mode);
? // Try the fast path of enq; backup to full enq on failure
? Node pred = tail;
? if (pred != null) {
? node.prev = pred;
? if (compareAndSetTail(pred, node)) {
? pred.next = node;
? return node;
? }
? }
? enq(node);
? return node;
}

上面是節(jié)點如隊列的一部分舷丹。當前僅當隊列不為空并且將新節(jié)點插入尾部成功后直接返回新節(jié)點抒钱。否則進入enq(Node)進行操作。

private Node enq(final Node node) {
? for (;;) {
? Node t = tail;
? if (t == null) { // Must initialize
? Node h = new Node(); // Dummy header
? h.next = node;
? node.prev = h;
? if (compareAndSetHead(h)) {
? tail = node;
? return h;
? }
? }
? else {
? node.prev = t;
? if (compareAndSetTail(t, node)) {
? t.next = node;
? return t;
? }
? }
? }
}

enq(Node)去隊列操作實現(xiàn)了CHL隊列的算法颜凯,如果為空就創(chuàng)建頭結(jié)點谋币,然后同時比較節(jié)點尾部是否是改變來決定CAS操作是否成功,當且僅當成功后才將為不節(jié)點的下一個節(jié)點指向為新節(jié)點症概±俣睿可以看到這里仍然是CAS操作。

acquireQueued(node,arg)

自旋請求鎖彼城,如果可能的話掛起線程诅蝶,直到得到鎖,返回當前線程是否中斷過(如果park()過并且中斷過的話有一個interrupted中斷位)募壕。

final boolean acquireQueued(final Node node, int arg) {
? try {
? boolean interrupted = false;
? for (;;) {
? final Node p = node.predecessor();
? if (p == head && tryAcquire(arg)) {
? setHead(node);
? p.next = null; // help GC
? return interrupted;
? }
? if (shouldParkAfterFailedAcquire(p, node) &&
? parkAndCheckInterrupt())
? interrupted = true;
? }
? } catch (RuntimeException ex) {
? cancelAcquire(node);
? throw ex;
? }
}

下面的分析就需要用到上節(jié)節(jié)點的狀態(tài)描述了调炬。acquireQueued過程是這樣的:

    1. 如果當前節(jié)點是AQS隊列的頭結(jié)點(如果第一個節(jié)點是DUMP節(jié)點也就是傀儡節(jié)點,那么第二個節(jié)點實際上就是頭結(jié)點了)司抱,就嘗試在此獲取鎖tryAcquire(arg)筐眷。如果成功就將頭結(jié)點設(shè)置為當前節(jié)點(不管第一個結(jié)點是否是DUMP節(jié)點),返回中斷位习柠。否則進行2匀谣。
    2. 檢測當前節(jié)點是否應該park(),如果應該park()就掛起當前線程并且返回當前線程中斷位资溃。進行操作1武翎。

一個節(jié)點是否該park()是關(guān)鍵,這是由方法java.util.concurrent.locks.AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire(Node, Node)實現(xiàn)的溶锭。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
? int s = pred.waitStatus;
? if (s < 0) return true;
? if (s > 0) {
? do {
? node.prev = pred = pred.prev;
? } while (pred.waitStatus > 0);
? pred.next = node;
? } else compareAndSetWaitStatus(pred, 0, Node.SIGNAL);
? return false;
}

    1. 如果前一個節(jié)點的等待狀態(tài)waitStatus<0宝恶,也就是前面的節(jié)點還沒有獲得到鎖,那么返回true,表示當前節(jié)點(線程)就應該park()了垫毙。否則進行2霹疫。
    2. 如果前一個節(jié)點的等待狀態(tài)waitStatus>0,也就是前一個節(jié)點被CANCELLED了综芥,那么就將前一個節(jié)點去掉丽蝎,遞歸此操作直到所有前一個節(jié)點的waitStatus<=0,進行4膀藐。否則進行3屠阻。
    3. 前一個節(jié)點等待狀態(tài)waitStatus=0,修改前一個節(jié)點狀態(tài)位為SINGAL额各,表示后面有節(jié)點等待你處理国觉,需要根據(jù)它的等待狀態(tài)來決定是否該park()。進行4虾啦。
    4. 返回false麻诀,表示線程不應該park()。

selfInterrupt()

private static void selfInterrupt() {
? Thread.currentThread().interrupt();
}

如果線程曾經(jīng)中斷過(或者阻塞過)(比如手動interrupt()或者超時等等缸逃,那么就再中斷一次针饥,中斷兩次的意思就是清除中斷位)。

大體上整個Lock.lock()就這樣一個流程需频。除了lock()方法外丁眼,還有l(wèi)ockInterruptibly()/tryLock()/unlock()/newCondition()等,在接下來的章節(jié)中會一一介紹昭殉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末苞七,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子挪丢,更是在濱河造成了極大的恐慌蹂风,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乾蓬,死亡現(xiàn)場離奇詭異惠啄,居然都是意外死亡,警方通過查閱死者的電腦和手機任内,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門撵渡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人死嗦,你說我怎么就攤上這事趋距。” “怎么了越除?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵节腐,是天一觀的道長外盯。 經(jīng)常有香客問我,道長翼雀,這世上最難降的妖魔是什么饱苟? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮锅纺,結(jié)果婚禮上掷空,老公的妹妹穿的比我還像新娘肋殴。我一直安慰自己囤锉,他們只是感情好,可當我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布护锤。 她就那樣靜靜地躺著官地,像睡著了一般。 火紅的嫁衣襯著肌膚如雪烙懦。 梳的紋絲不亂的頭發(fā)上驱入,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機與錄音氯析,去河邊找鬼亏较。 笑死,一個胖子當著我的面吹牛掩缓,可吹牛的內(nèi)容都是我干的雪情。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼你辣,長吁一口氣:“原來是場噩夢啊……” “哼巡通!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起舍哄,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤宴凉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后表悬,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弥锄,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年蟆沫,在試婚紗的時候發(fā)現(xiàn)自己被綠了籽暇。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡饥追,死狀恐怖图仓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情但绕,我是刑警寧澤救崔,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布惶看,位于F島的核電站,受9級特大地震影響六孵,放射性物質(zhì)發(fā)生泄漏纬黎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一劫窒、第九天 我趴在偏房一處隱蔽的房頂上張望本今。 院中可真熱鬧,春花似錦主巍、人聲如沸冠息。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽逛艰。三九已至,卻和暖如春搞旭,著一層夾襖步出監(jiān)牢的瞬間散怖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工肄渗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留镇眷,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓翎嫡,卻偏偏與公主長得像欠动,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子钝的,可洞房花燭夜當晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容