摘要:鎖與很好的隔離使用者與實(shí)現(xiàn)者所需要關(guān)注的領(lǐng)域。那么這個(gè)就是包裝線程并且放入到隊(duì)列的過程實(shí)現(xiàn)的方法。也證實(shí)了就是獲取鎖的線程的節(jié)點(diǎn)。如果發(fā)生異常取消請(qǐng)求,也就是將當(dāng)前節(jié)點(diǎn)重隊(duì)列中移除。
前言
自從JDK1.5后,jdk新增一個(gè)并發(fā)工具包java.util.concurrent,提供了一系列的并發(fā)工具類。而今天我們需要學(xué)習(xí)的是java.util.concurrent.lock也就是它下面的lock包,其中有一個(gè)最為常見類ReentrantLock,
我們知道ReentrantLock的功能是實(shí)現(xiàn)代碼段的并發(fā)訪問控制,也就是通常意義上所說的鎖。之前我們也學(xué)習(xí)過一種鎖的實(shí)現(xiàn),也就是synchronized關(guān)鍵詞,synchronized是在字節(jié)碼層面,通過對(duì)象的監(jiān)視器鎖實(shí)現(xiàn)的。那么ReentrantLock又是怎么實(shí)現(xiàn)的呢?
如果不看源碼,可能會(huì)以為它的實(shí)現(xiàn)是通過類似于synchronized,通過對(duì)象的監(jiān)視器鎖實(shí)現(xiàn)的。但事實(shí)上它僅僅是一個(gè)工具類!沒有使用更“高級(jí)”的機(jī)器指令,不是關(guān)鍵字,也不依靠JDK編譯時(shí)的特殊處理,僅僅作為一個(gè)普普通通的類就完成了代碼塊的并發(fā)訪問控制,這就更讓人疑問它怎么實(shí)現(xiàn)的代碼塊的并發(fā)訪問控制的了。
我們查看源碼發(fā)現(xiàn),它是通過繼承抽象類實(shí)現(xiàn)的AbstractQueuedSynchronizer,為了方便描述,接下來我將用AQS代替AbstractQueuedSynchronizer。
關(guān)于AQSAQS,它是用來構(gòu)建鎖或者其他同步組建的基礎(chǔ)框架,我們見過許多同步工具類都是基于它構(gòu)建的。包括ReentrantLock、CountDownLatch等。在深入了解AQS了解之前,我們需要知道鎖跟AQS的區(qū)別。鎖,它是面向使用者的,它定義了使用者與鎖交互的接口,隱藏了實(shí)現(xiàn)的細(xì)節(jié);而AQS面像的是鎖的實(shí)現(xiàn)者,它簡(jiǎn)化了鎖的實(shí)現(xiàn)。鎖與AQS很好的隔離使用者與實(shí)現(xiàn)者所需要關(guān)注的領(lǐng)域。那么我們今天就作為一個(gè)鎖的實(shí)現(xiàn)者,一步一步分析鎖的實(shí)現(xiàn)。
AQS又稱同步器,它的內(nèi)部有一個(gè)int成員變量state表示同步狀態(tài),還有一個(gè)內(nèi)置的FIFO隊(duì)列來實(shí)現(xiàn)資源獲取線程的排隊(duì)工作。通過它們我們就能實(shí)現(xiàn)鎖。
在實(shí)現(xiàn)鎖之前,我們需要考慮做為鎖的使用者,鎖會(huì)有哪幾種?
通常來說,鎖分為兩種,一種是獨(dú)占鎖(排它鎖,互斥鎖),另一種就是共享鎖了。根據(jù)這兩類,其實(shí)AQS也給我們提供了兩套API。而我們作為鎖的實(shí)現(xiàn)者,通常都是要么全部實(shí)現(xiàn)它的獨(dú)占api,要么實(shí)現(xiàn)它的共享api,而不會(huì)出現(xiàn)一起實(shí)現(xiàn)的。即使juc內(nèi)置的ReentrantReadWriteLock也是通過兩個(gè)子類分別來實(shí)現(xiàn)的。
鎖的實(shí)現(xiàn) 獨(dú)占鎖獨(dú)占鎖又名互斥鎖,同一時(shí)間,只有一個(gè)線程能獲取到鎖,其余的線程都會(huì)被阻塞等待。其中我們常用的ReentrantLock就是一種獨(dú)占鎖,我們一起來ReentrantLock 分析ReentrantLock的同時(shí)看一看AQS的實(shí)現(xiàn),再推理出AQS獨(dú)特的設(shè)計(jì)思路和實(shí)現(xiàn)方式。最后,再看其共享控制功能的實(shí)現(xiàn)。
首先我們來看看獲取鎖的過程
加鎖我們查看ReentrantLock的源碼。來分析它的lock方法
public void lock() { sync.lock(); }
與我們之前分析的一樣,鎖的具體實(shí)現(xiàn)由內(nèi)部的代理類完成,lock只是暴露給鎖的使用者的一套api。使用過ReentrantLock的同學(xué)應(yīng)該知道,ReentrantLock又分為公平鎖和非公平鎖,所以,ReentrantLock內(nèi)部只有兩個(gè)sync的實(shí)現(xiàn)。
/** * Sync object for non-fair locks */ static final class NonfairSync extends Sync{..} /** * Sync object for fair locks */ static final class FairSync extends Sync{..}
公平鎖 :每個(gè)線程獲取鎖的順序是按照調(diào)用lock方法的先后順序來的。
非公平鎖:每個(gè)線程獲取鎖的順序是不會(huì)按照調(diào)用lock方法的先后順序來的。完全看運(yùn)氣。
所以我們完全可以猜測(cè)到,這個(gè)公平與不公平的區(qū)別就體現(xiàn)在鎖的獲取過程。我們以公平鎖為例,來分析獲取鎖過程,最后對(duì)比非公平鎖的過程,尋找差異。
lock查看FairSync的lock方法
final void lock() { acquire(1); }
這里它調(diào)用到了父類AQS的acquire方法,所以我們繼續(xù)查看acquire方法的代碼
acquire/** * Acquires in exclusive mode, ignoring interrupts. Implemented * by invoking at least once {@link #tryAcquire}, * returning on success. Otherwise the thread is queued, possibly * repeatedly blocking and unblocking, invoking {@link * #tryAcquire} until success. This method can be used * to implement method {@link Lock#lock}. * * @param arg the acquire argument. This value is conveyed to * {@link #tryAcquire} but is otherwise uninterpreted and * can represent anything you like. */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
查看方法方法的注釋我們可以知道這個(gè)方法的作用,這里我簡(jiǎn)單的翻譯一下.
Acquires方法是一個(gè)獨(dú)占鎖模式的方法,它是不會(huì)響應(yīng)中斷的。它至少執(zhí)行一次tryAcquire去獲取鎖,如果返回true,則代表獲取鎖成功,否則它將會(huì)被加入等待隊(duì)列阻塞,直到重新嘗試獲取鎖成功。所以我們需要看看嘗試獲取鎖的方法tryAcquire的實(shí)現(xiàn)
tryAcruireprotected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
拋出一個(gè)異常,沒有實(shí)現(xiàn)。所以我們需要查看它的子類,在我們這里就是FairSync的實(shí)現(xiàn)。
這里也會(huì)大家會(huì)有疑惑,沒有實(shí)現(xiàn)為什么不寫成抽象方法呢,前面我們提到過,我們不會(huì)同時(shí)在一個(gè)類中實(shí)現(xiàn)獨(dú)占鎖跟共享鎖的api,那么tryAcruire是屬于獨(dú)占鎖,那么如果我想一個(gè)共享鎖也要重新獨(dú)占鎖的方法嗎?所以大師的設(shè)計(jì)是絕對(duì)沒有問題的。
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread();//獲取當(dāng)前線程 int c = getState(); //獲取父類AQS中的標(biāo)志位 if (c == 0) { if (!hasQueuedPredecessors() && //如果隊(duì)列中沒有其他線程 說明沒有線程正在占有鎖! compareAndSetState(0, acquires)) { //修改一下狀態(tài)位,注意:這里的acquires是在lock的時(shí)候傳遞來的,從上面的圖中可以知道,這個(gè)值是寫死的1 setExclusiveOwnerThread(current); //如果通過CAS操作將狀態(tài)為更新成功則代表當(dāng)前線程獲取鎖,因此,將當(dāng)前線程設(shè)置到AQS的一個(gè)變量中,說明這個(gè)線程拿走了鎖。 return true; } } else if (current == getExclusiveOwnerThread()) { //如果不為0 意味著,鎖已經(jīng)被拿走了,但是,因?yàn)镽eentrantLock是重入鎖, //是可以重復(fù)lock,unlock的,只要成對(duì)出現(xiàn)行。一次。這里還要再判斷一次 獲取鎖的線程是不是當(dāng)前請(qǐng)求鎖的線程。 int nextc = c + acquires;//如果是的,累加在state字段上就可以了。 if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
目前為止,如果獲取鎖成功,則返回true,獲取鎖的過程結(jié)束,如果獲取失敗,則返回false
按照之前的邏輯,如果線程獲取鎖失敗,則會(huì)被放入到隊(duì)列中,但是在放入之前,需要給線程包裝一下。
那么這個(gè)addWaiter就是包裝線程并且放入到隊(duì)列的過程實(shí)現(xiàn)的方法。
addWaiter/** * Creates and enqueues node for current thread and given mode. * * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared * @return the new node */ 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; }
注釋: 把當(dāng)前線程作為一個(gè)節(jié)點(diǎn)添加到隊(duì)列中,并且為這個(gè)節(jié)點(diǎn)設(shè)置模式
模式: 也就是獨(dú)占模式/共享模式,在這里模式是形參,所以我們看看起調(diào)方acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) Node.EXCLUSIVE 就代表這是獨(dú)占鎖模式。
創(chuàng)建好節(jié)點(diǎn)后,將節(jié)點(diǎn)加入到隊(duì)列尾部,此處,在隊(duì)列不為空的時(shí)候,先嘗試通過cas方式修改尾節(jié)點(diǎn)為最新的節(jié)點(diǎn),如果修改失敗,意味著有并發(fā),這個(gè)時(shí)候才會(huì)進(jìn)入enq中死循環(huán),“自旋”方式修改。
將線程的節(jié)點(diǎn)接入到隊(duì)里中后,當(dāng)然還需要做一件事:將當(dāng)前線程掛起!這個(gè)事,由acquireQueued來做。
在解釋acquireQueued之前,我們需要先看下AQS中隊(duì)列的內(nèi)存結(jié)構(gòu),我們知道,隊(duì)列由Node類型的節(jié)點(diǎn)組成,其中至少有兩個(gè)變量,一個(gè)封裝線程,一個(gè)封裝節(jié)點(diǎn)類型。
而實(shí)際上,它的內(nèi)存結(jié)構(gòu)是這樣的(第一次節(jié)點(diǎn)插入時(shí),第一個(gè)節(jié)點(diǎn)是一個(gè)空節(jié)點(diǎn),代表有一個(gè)線程已經(jīng)獲取鎖,事實(shí)上,隊(duì)列的第一個(gè)節(jié)點(diǎn)就是代表持有鎖的節(jié)點(diǎn)):
黃色節(jié)點(diǎn)為隊(duì)列默認(rèn)的頭節(jié)點(diǎn),每次有線程競(jìng)爭(zhēng)失敗,進(jìn)入隊(duì)列后其實(shí)都是插入到隊(duì)列的尾節(jié)點(diǎn)(tail后面)后面。這個(gè)從enq方法可以看出來,上文中有提到enq方法為將節(jié)點(diǎn)插入隊(duì)列的方法:
enqprivate Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize // 一個(gè)空的節(jié)點(diǎn),通常代表獲取鎖的線程 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }acquireQueued
接著我們來看看當(dāng)節(jié)點(diǎn)被放入到隊(duì)列中,如何將線程掛起,也就是看看acquireQueued方法的實(shí)現(xiàn)。
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { // 獲取當(dāng)前節(jié)點(diǎn)前驅(qū)結(jié)點(diǎn) final Node p = node.predecessor(); // 如果前驅(qū)節(jié)點(diǎn)是head,那么它就是等待隊(duì)列中的第一個(gè)線程 // 因?yàn)槲覀冎纇ead就是獲取線程的節(jié)點(diǎn),那么它就有機(jī)會(huì)再次獲取鎖 if (p == head && tryAcquire(arg)) { //成功后,將上圖中的黃色節(jié)點(diǎn)移除,Node1變成頭節(jié)點(diǎn)。 也證實(shí)了head就是獲取鎖的線程的節(jié)點(diǎn)。 setHead(node); p.next = null; // help GC failed = false; return interrupted; } // 1、檢查前一個(gè)節(jié)點(diǎn)的狀態(tài),判斷是否要掛起 // 2、如果需要掛起,則通過JUC下的LockSopport類的靜態(tài)方法park掛起當(dāng)前線程,直到被喚醒。 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { // 如果發(fā)生異常 if (failed) // 取消請(qǐng)求,也就是將當(dāng)前節(jié)點(diǎn)重隊(duì)列中移除。 cancelAcquire(node); } }
這里我還需要解釋的是:
1、Node節(jié)點(diǎn)除了存儲(chǔ)當(dāng)前線程之外,節(jié)點(diǎn)類型,前驅(qū)后驅(qū)指針之后,還存儲(chǔ)一個(gè)叫waitStatus的變量,該變量用于描述節(jié)點(diǎn)的狀態(tài)。共有四種狀態(tài)。
/** waitStatus value to indicate thread has cancelled */ static final int CANCELLED = 1; /** waitStatus value to indicate successor"s thread needs unparking */ static final int SIGNAL = -1; /** waitStatus value to indicate thread is waiting on condition */ static final int CONDITION = -2; /** * waitStatus value to indicate the next acquireShared should * unconditionally propagate */ static final int PROPAGATE = -3;
分別表示:
1 = 取消狀態(tài),該節(jié)點(diǎn)將會(huì)被隊(duì)列移除。
-1 = 等待狀態(tài),后驅(qū)節(jié)點(diǎn)處于等待狀態(tài)。
-2 = 等待被通知,該節(jié)點(diǎn)將會(huì)阻塞至被該鎖的condition的await方法喚醒。
-3 = 共享傳播狀態(tài),代表該節(jié)點(diǎn)的狀態(tài)會(huì)向后傳播。
到此為止,一個(gè)線程對(duì)于鎖的一次競(jìng)爭(zhēng)才告于段落,結(jié)果有兩種,要么成功獲取到鎖(不用進(jìn)入到AQS隊(duì)列中),要么,獲取失敗,被掛起,等待下次喚醒后繼續(xù)循環(huán)嘗試獲取鎖,值得注意的是,AQS的隊(duì)列為FIFO隊(duì)列,所以,每次被CPU假喚醒,且當(dāng)前線程不是出在頭節(jié)點(diǎn)的位置,也是會(huì)被掛起的。AQS通過這樣的方式,實(shí)現(xiàn)了競(jìng)爭(zhēng)的排隊(duì)策略。
釋放鎖看完了加鎖,再看釋放鎖。我們先不看代碼也可以猜測(cè)到釋放鎖需要的步驟。
隊(duì)列的頭節(jié)點(diǎn)是當(dāng)前獲取鎖的線程,所以我們需要移除頭節(jié)點(diǎn)
釋放鎖,喚醒頭節(jié)點(diǎn)后驅(qū)節(jié)點(diǎn)來競(jìng)爭(zhēng)鎖
接下來我們查看源碼來驗(yàn)證我們的猜想是否在正確。
unlockpublic void unlock() { sync.release(1); }
unlock方法調(diào)用AQS的release方法,因?yàn)槲覀兊腶cquire的時(shí)候傳入的是1,也就是同步狀態(tài)量+1,那么對(duì)應(yīng)的解鎖就要-1。
releasepublic final boolean release(int arg) { // 嘗試釋放鎖 if (tryRelease(arg)) { // 釋放鎖成功,獲取當(dāng)前隊(duì)列的頭節(jié)點(diǎn) Node h = head; if (h != null && h.waitStatus != 0) // 喚醒當(dāng)前節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn) unparkSuccessor(h); return true; } return false; }tryRelease
同樣的它是交給子類實(shí)現(xiàn)的
protected final boolean tryRelease(int releases) { int c = getState() - releases; // 當(dāng)前線程不是獲取鎖的線程 拋出異常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 因?yàn)槭侵厝氲年P(guān)系,不是每次釋放鎖c都等于0,直到最后一次釋放鎖時(shí),才通知AQS不需要再記錄哪個(gè)線程正在獲取鎖。 if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }unparkSuccessor
釋放鎖成功之后,就喚醒頭節(jié)點(diǎn)后驅(qū)節(jié)點(diǎn)來競(jìng)爭(zhēng)鎖
private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }
值得注意的是,尋找的順序是從隊(duì)列尾部開始往前去找的最前面的一個(gè)waitStatus小于0的節(jié)點(diǎn)。因?yàn)榇笥? 就是1狀態(tài)的節(jié)點(diǎn)是取消狀態(tài)。
公平鎖與非公平鎖到此我們鎖獲取跟鎖的釋放已經(jīng)分析的差不多。那么公平鎖跟非公平鎖的區(qū)別在于加鎖的過程。對(duì)比代碼
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } }
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); } }
從代碼中也可以看出來,非公平在公平鎖的加鎖的邏輯之前先直接cas修改一次state變量(嘗試獲取鎖),成功就返回,不成功再排隊(duì),從而達(dá)到不排隊(duì)直接搶占的目的。
最后歡迎大家關(guān)注一下我的個(gè)人公眾號(hào)。一起交流一起學(xué)習(xí),有問必答。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/77100.html
摘要:今天給大家總結(jié)一下,面試中出鏡率很高的幾個(gè)多線程面試題,希望對(duì)大家學(xué)習(xí)和面試都能有所幫助。指令重排在單線程環(huán)境下不會(huì)出先問題,但是在多線程環(huán)境下會(huì)導(dǎo)致一個(gè)線程獲得還沒有初始化的實(shí)例。使用可以禁止的指令重排,保證在多線程環(huán)境下也能正常運(yùn)行。 下面最近發(fā)的一些并發(fā)編程的文章匯總,通過閱讀這些文章大家再看大廠面試中的并發(fā)編程問題就沒有那么頭疼了。今天給大家總結(jié)一下,面試中出鏡率很高的幾個(gè)多線...
摘要:的主要功能和關(guān)鍵字一致,均是用于多線程的同步。而僅支持通過查詢當(dāng)前線程是否持有鎖。由于和使用的是同一把可重入鎖,所以線程可以進(jìn)入方法,并再次獲得鎖,而不會(huì)被阻塞住。公平與非公平公平與非公平指的是線程獲取鎖的方式。 1.簡(jiǎn)介 可重入鎖ReentrantLock自 JDK 1.5 被引入,功能上與synchronized關(guān)鍵字類似。所謂的可重入是指,線程可對(duì)同一把鎖進(jìn)行重復(fù)加鎖,而不會(huì)被阻...
摘要:開始獲取鎖終于輪到出場(chǎng)了,的調(diào)用過程和完全一樣,同樣拿不到鎖,然后加入到等待隊(duì)列隊(duì)尾然后,在阻塞前需要把前驅(qū)結(jié)點(diǎn)的狀態(tài)置為,以確保將來可以被喚醒至此,的執(zhí)行也暫告一段落了安心得在等待隊(duì)列中睡覺。 showImg(https://segmentfault.com/img/remote/1460000016012467); 本文首發(fā)于一世流云的專欄:https://segmentfault...
摘要:撤銷鎖偏向鎖使用了一種等到競(jìng)爭(zhēng)出現(xiàn)才釋放鎖的機(jī)制,所以當(dāng)其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖。輕量級(jí)鎖線程在執(zhí)行同步代碼塊之前,會(huì)先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲(chǔ)鎖記錄的空間,并將對(duì)象頭中的復(fù)制到鎖記錄中,官方稱為。 前言 作者前面也寫了幾篇關(guān)于Java并發(fā)編程,以及線程和volatil的基礎(chǔ)知識(shí),有興趣可以閱讀作者的原文博客,今天關(guān)于Java中的兩種鎖進(jìn)行詳解,希望對(duì)...
閱讀 2727·2021-11-23 09:51
閱讀 3319·2021-11-22 14:44
閱讀 4664·2021-11-22 09:34
閱讀 5404·2021-10-08 10:14
閱讀 2789·2021-09-22 15:47
閱讀 3588·2021-09-22 15:40
閱讀 1575·2019-08-30 15:44
閱讀 1689·2019-08-28 18:23