您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關Java并發之AbstractQueuedSynchronizer源碼獨占模式的示例分析的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
獨占模式下結點是怎樣進入同步隊列排隊的,以及離開同步隊列之前會進行哪些操作。AQS為在獨占模式和共享模式下獲取鎖分別提供三種獲取方式:不響應線程中斷獲取,響應線程中斷獲取,設置超時時間獲取。這三種方式整體步驟大致是相同的,只有少部分不同的地方,所以理解了一種方式再看其他方式的實現都是大同小異。在本篇中我會著重講不響應線程中斷的獲取方式,其他兩種方式也會順帶講一下不一致的地方。
1. 怎樣以不響應線程中斷獲取鎖?
//不響應中斷方式獲取(獨占模式) public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { selfInterrupt(); } }
上面代碼中雖然看起來簡單,但是它按照順序執行了下圖所示的4個步驟。下面我們會逐個步驟進行演示分析。
第一步:!tryAcquire(arg)
//嘗試去獲取鎖(獨占模式) protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
這時候來了一個人,他首先嘗試著去敲了敲門,如果發現門沒鎖(tryAcquire(arg)=true),那就直接進去了。如果發現門鎖了(tryAcquire(arg)=false),就執行下一步。這個tryAcquire方法決定了什么時候鎖是開著的,什么時候鎖是關閉的。這個方法必須要讓子類去覆蓋,重寫里面的判斷邏輯。
第二步:addWaiter(Node.EXCLUSIVE)
//將當前線程包裝成結點并添加到同步隊列尾部 private Node addWaiter(Node mode) { //指定持有鎖的模式 Node node = new Node(Thread.currentThread(), mode); //獲取同步隊列尾結點引用 Node pred = tail; //如果尾結點不為空, 表明同步隊列已存在結點 if (pred != null) { //1.指向當前尾結點 node.prev = pred; //2.設置當前結點為尾結點 if (compareAndSetTail(pred, node)) { //3.將舊的尾結點的后繼指向新的尾結點 pred.next = node; return node; } } //否則表明同步隊列還沒有進行初始化 enq(node); return node; } //結點入隊操作 private Node enq(final Node node) { for (;;) { //獲取同步隊列尾結點引用 Node t = tail; //如果尾結點為空說明同步隊列還沒有初始化 if (t == null) { //初始化同步隊列 if (compareAndSetHead(new Node())) { tail = head; } } else { //1.指向當前尾結點 node.prev = t; //2.設置當前結點為尾結點 if (compareAndSetTail(t, node)) { //3.將舊的尾結點的后繼指向新的尾結點 t.next = node; return t; } } } }
執行到這一步表明第一次獲取鎖失敗,那么這個人就給自己領了塊號碼牌進入排隊區去排隊了,在領號碼牌的時候會聲明自己想要以什么樣的方式來占用房間(獨占模式or共享模式)。注意,這時候他并沒有坐下來休息(將自己掛起)哦。
第三步:acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
//以不可中斷方式獲取鎖(獨占模式) final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { //獲取給定結點的前繼結點的引用 final Node p = node.predecessor(); //如果當前結點是同步隊列的第一個結點, 就嘗試去獲取鎖 if (p == head && tryAcquire(arg)) { //將給定結點設置為head結點 setHead(node); //為了幫助垃圾收集, 將上一個head結點的后繼清空 p.next = null; //設置獲取成功狀態 failed = false; //返回中斷的狀態, 整個循環執行到這里才是出口 return interrupted; } //否則說明鎖的狀態還是不可獲取, 這時判斷是否可以掛起當前線程 //如果判斷結果為真則掛起當前線程, 否則繼續循環, 在這期間線程不響應中斷 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) { interrupted = true; } } } finally { //在最后確保如果獲取失敗就取消獲取 if (failed) { cancelAcquire(node); } } } //判斷是否可以將當前結點掛起 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //獲取前繼結點的等待狀態 int ws = pred.waitStatus; //如果前繼結點狀態為SIGNAL, 表明前繼結點會喚醒當前結點, 所以當前結點可以安心的掛起了 if (ws == Node.SIGNAL) { return true; } if (ws > 0) { //下面的操作是清理同步隊列中所有已取消的前繼結點 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //到這里表示前繼結點狀態不是SIGNAL, 很可能還是等于0, 這樣的話前繼結點就不會去喚醒當前結點了 //所以當前結點必須要確保前繼結點的狀態為SIGNAL才能安心的掛起自己 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } //掛起當前線程 private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
領完號碼牌進入排隊區后就會立馬執行這個方法,當一個結點首次進入排隊區后有兩種情況,一種是發現他前面的那個人已經離開座位進入房間了,那他就不坐下來休息了,會再次去敲一敲門看看那小子有沒有完事。如果里面的人剛好完事出來了,都不用他叫自己就直接沖進去了。否則,就要考慮坐下來休息一會兒了,但是他還是不放心,如果他坐下來睡著后沒人提醒他怎么辦?他就在前面那人的座位上留一個小紙條,好讓從里面出來的人看到紙條后能夠喚醒他。還有一種情況是,當他進入排隊區后發現前面還有好幾個人在座位上排隊呢,那他就可以安心的坐下來咪一會兒了,但在此之前他還是會在前面那人(此時已經睡著了)的座位上留一個紙條,好讓這個人在走之前能夠去喚醒自己。當一切事情辦妥了之后,他就安安心心的睡覺了,注意,我們看到整個for循環就只有一個出口,那就是等線程成功的獲取到鎖之后才能出去,在沒有獲取到鎖之前就一直是掛在for循環的parkAndCheckInterrupt()方法里頭。線程被喚醒后也是從這個地方繼續執行for循環。
第四步:selfInterrupt()
//當前線程將自己中斷 private static void selfInterrupt() { Thread.currentThread().interrupt(); }
由于上面整個線程一直是掛在for循環的parkAndCheckInterrupt()方法里頭,沒有成功獲取到鎖之前不響應任何形式的線程中斷,只有當線程成功獲取到鎖并從for循環出來后,他才會查看在這期間是否有人要求中斷線程,如果是的話再去調用selfInterrupt()方法將自己掛起。
2. 怎樣以響應線程中斷獲取鎖?
//以可中斷模式獲取鎖(獨占模式) private void doAcquireInterruptibly(int arg) throws InterruptedException { //將當前線程包裝成結點添加到同步隊列中 final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { //獲取當前結點的前繼結點 final Node p = node.predecessor(); //如果p是head結點, 那么當前線程就再次嘗試獲取鎖 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; //獲取鎖成功后返回 return; } //如果滿足條件就掛起當前線程, 此時響應中斷并拋出異常 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) { //線程被喚醒后如果發現中斷請求就拋出異常 throw new InterruptedException(); } } } finally { if (failed) { cancelAcquire(node); } } }
響應線程中斷方式和不響應線程中斷方式獲取鎖流程上大致上是相同的。唯一的一點區別就是線程從parkAndCheckInterrupt方法中醒來后會檢查線程是否中斷,如果是的話就拋出InterruptedException異常,而不響應線程中斷獲取鎖是在收到中斷請求后只是設置一下中斷狀態,并不會立馬結束當前獲取鎖的方法,一直到結點成功獲取到鎖之后才會根據中斷狀態決定是否將自己掛起。
3. 怎樣設置超時時間獲取鎖?
//以限定超時時間獲取鎖(獨占模式) private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { //獲取系統當前時間 long lastTime = System.nanoTime(); //將當前線程包裝成結點添加到同步隊列中 final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { //獲取當前結點的前繼結點 final Node p = node.predecessor(); //如果前繼是head結點, 那么當前線程就再次嘗試獲取鎖 if (p == head && tryAcquire(arg)) { //更新head結點 setHead(node); p.next = null; failed = false; return true; } //超時時間用完了就直接退出循環 if (nanosTimeout <= 0) { return false; } //如果超時時間大于自旋時間, 那么等判斷可以掛起線程之后就會將線程掛起一段時間 if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) { //將當前線程掛起一段時間, 之后再自己醒來 LockSupport.parkNanos(this, nanosTimeout); } //獲取系統當前時間 long now = System.nanoTime(); //超時時間每次都減去獲取鎖的時間間隔 nanosTimeout -= now - lastTime; //再次更新lastTime lastTime = now; //在獲取鎖的期間收到中斷請求就拋出異常 if (Thread.interrupted()) { throw new InterruptedException(); } } } finally { if (failed) { cancelAcquire(node); } } }
設置超時時間獲取首先會去獲取一下鎖,第一次獲取鎖失敗后會根據情況,如果傳入的超時時間大于自旋時間那么就會將線程掛起一段時間,否則的話就會進行自旋,每次獲取鎖之后都會將超時時間減去獲取一次鎖所用的時間。一直到超時時間小于0也就說明超時時間用完了,那么這時就會結束獲取鎖的操作然后返回獲取失敗標志。注意在以超時時間獲取鎖的過程中是可以響應線程中斷請求的。
4. 線程釋放鎖并離開同步隊列是怎樣進行的?
//釋放鎖的操作(獨占模式) public final boolean release(int arg) { //撥動密碼鎖, 看看是否能夠開鎖 if (tryRelease(arg)) { //獲取head結點 Node h = head; //如果head結點不為空并且等待狀態不等于0就去喚醒后繼結點 if (h != null && h.waitStatus != 0) { //喚醒后繼結點 unparkSuccessor(h); } return true; } return false; } //喚醒后繼結點 private void unparkSuccessor(Node node) { //獲取給定結點的等待狀態 int ws = node.waitStatus; //將等待狀態更新為0 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); } }
線程持有鎖進入房間后就會去辦自己的事情,等事情辦完后它就會釋放鎖并離開房間。通過tryRelease方法可以撥動密碼鎖進行解鎖,我們知道tryRelease方法是需要讓子類去覆蓋的,不同的子類實現的規則不一樣,也就是說不同的子類設置的密碼不一樣。像在ReentrantLock當中,房間里面的人每調用tryRelease方法一次,state就減1,直到state減到0的時候密碼鎖就開了。大家想想這個過程像不像我們在不停的轉動密碼鎖的轉輪,而每次轉動轉輪數字只是減少1。CountDownLatch和這個也有點類似,只不過它不是一個人在轉,而是多個人每人都去轉一下,集中大家的力量把鎖給開了。線程出了房間后它會找到自己原先的座位,也就是找到head結點。看看座位上有沒有人給它留了小紙條,如果有的話它就知道有人睡著了需要讓它幫忙喚醒,那么它就會去喚醒那個線程。如果沒有的話就表明同步隊列中暫時還沒有人在等待,也沒有人需要它喚醒,所以它就可以安心的離去了。以上過程就是在獨占模式下釋放鎖的過程。
感謝各位的閱讀!關于“Java并發之AbstractQueuedSynchronizer源碼獨占模式的示例分析”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。