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