1. 程式人生 > >Java讀原始碼之ReentrantLock(2)

Java讀原始碼之ReentrantLock(2)

前言

本文是 ReentrantLock 原始碼的第二篇,第一篇主要介紹了公平鎖非公平鎖正常的加鎖解鎖流程,雖然表達能力有限不知道有沒有講清楚,本著不太監的原則,本文填補下第一篇中挖的坑。

Java讀原始碼之ReentrantLock

原始碼分析

感知中斷鎖

如果我們希望檢測到中斷後能立刻丟擲異常就用 lockInterruptibly 方法去加鎖,還是建議用 lock 方法,自定義中斷處理,更靈活一點。

  • ReentrantLock#lockInterruptibly

我們只需要把 ReentrantLock#lock 改成 ReentrantLock#lockInterruptibly 方法就可以獲得內部檢測中斷的鎖了

public void lockInterruptibly() throws InterruptedException {
  sync.acquireInterruptibly(1);
}
  • AbstractQueuedSynchronizer#acquireInterruptibly

主要流程和前文介紹的類似

public final void acquireInterruptibly(int arg)
  throws InterruptedException {
  // 一上來就檢查下中斷,中斷直接異常,就沒必要搶鎖排隊了
  if (Thread.interrupted())
    throw new InterruptedException();
  if (!tryAcquire(arg))
    doAcquireInterruptibly(arg);
}
  • AbstractQueuedSynchronizer#doAcquireInterruptibly

和正常加鎖唯一區別就是這個方法,但是定睛一看是不是似曾相識?最大區別就是把中斷標識給去掉了,檢測到中斷直接拋異常

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
  	// 大神也偷懶了,因為這個方法,只有獨佔鎖且檢查中斷這一個應用場景,把節點入隊的步驟也揉了進來
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            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);
    }
}
  • AbstractQueuedSynchronizer#cancelAcquire

此方法很有東西,只保證該節點失效,然後延遲移出等待佇列

private void cancelAcquire(Node node) {
    if (node == null)
        return;
		// 把節點裡登記等待的執行緒去掉,完成這一步此節點已經沒有作用了
    node.thread = null;

    // 下面的三步其實可以放到一個CAS中,直接設定 CANCELLED 狀態 ,拿前一個節點,predNext 也必然是自己,但是吞吐量就下來了
    // 這裡大神,沒有這樣做也是出於了效能考慮,因為我們已經把等待執行緒設定成 null 了,所以此節點已經沒有任何意義,沒有必要去保證節點第一時間被釋放,只要設定好 CANCELLED 狀態
    // 就算後面 CAS 調整等待佇列失敗了,下次取消操作也會幫著回收。相應地程式碼複雜度提高了。
  
    /* ----------------------------------------- */
    // 找到自己前面第一個沒取消的節點,
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    // 主要是為了下面把連結串列接上
    Node predNext = pred.next;
    // 這裡邏輯上把當前節點的狀態設定成取消,便於檢測釋放
    node.waitStatus = Node.CANCELLED;
  	/* ----------------------------------------- */
  
    // 如果當前節點是尾節點,就把前一個沒取消的節點設成新尾巴
    if (node == tail && compareAndSetTail(node, pred)) {
      	// 把新尾巴的 next 設定成空
        compareAndSetNext(pred, predNext, null);
    } else {
        // 進到這裡說明當前節點肯定不是尾節點了
        int ws;
      	// 條件1: 如果前一個非取消節點不是頭,也就是還需要排隊
      	// 條件2: 如果前一個節點為 SIGNAL,也就是說後面肯定還有執行緒等待被喚醒
      	// 條件3: 如果前一個節點也取消了,說明前一個節點也取消了,還沒來得及設定狀態
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
              	// 當前節點後一個沒取消的話,就接到前一個正常的節點後面
                compareAndSetNext(pred, predNext, next);
        } else {
          	// 前一篇文章解鎖部分講過,會把下一個節點中的執行緒恢復,然後把後繼節點接上
            unparkSuccessor(node);
        }

      	// 有點花裡胡哨,直接 = null不行麼,
        node.next = node; // help GC
    }
}

來張圖說明下,假如我們目前等待佇列裡有7個執行緒:

等待條件鎖

上篇文章看原始碼過程中,AQS中有個 CONDITION 狀態沒有研究

static final int CONDITION = -2;

ReentrantLock 中的 newCondition 等 Condition 相關方法正是基於 AQS 中的實現的,讓我們先大致瞭解一波作用和用法

Condition簡介

Condition 類似於 Object 中的 wait 和 notify ,主要用於執行緒間通訊,最大的優勢是 Object 的 wait 是把執行緒放到當前物件的等待池中,也就是說一個物件只能有一個等待條件,而 Condition 可以支援多個等待條件,舉個例子,商品要等至少三個人預定了才開始發售,第一個預定的減500,第二三兩個減100。正式發售之後恢復原價。

public class ReentrantLockConditionDemo {

    private final ReentrantLock reentrantLock = new ReentrantLock();
    private final Condition wait1 = reentrantLock.newCondition();
    private final Condition wait2 = reentrantLock.newCondition();
    private int wait1Count = 0;
    private int wait2Count = 0;

    public void buy() {
        int price = 999;
        reentrantLock.lock();
        try {
            while (wait1Count++ < 1) {
                System.out.println(Thread.currentThread().getName() + "減500");
                wait1.await();
                price -= 500;
            }
            wait1.signal();
            while (wait2Count++ < 2) {
                System.out.println(Thread.currentThread().getName() + "減100");
                wait2.await();
                price -= 100;
            }
            wait2.signal();
            System.out.println(Thread.currentThread().getName() + "到手價" + price);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantLock.unlock();
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        ReentrantLockConditionDemo reentrantLockConditionDemo = new ReentrantLockConditionDemo();
        IntStream.rangeClosed(0, 4)
                .forEach(num -> executorService
                        .execute(reentrantLockConditionDemo::buy)
                );
    }
  
    /**
     * 輸出:
     *
     * pool-1-thread-1減500
     * pool-1-thread-2減100
     * pool-1-thread-3減100
     * pool-1-thread-4到手價999
     * pool-1-thread-5到手價999
     * pool-1-thread-1到手價499
     * pool-1-thread-2到手價899
     * pool-1-thread-3到手價899
     */
}
  • ReentrantLock#newCondition

先來看條件的建立,需要基於鎖物件使用 newCondition 去建立

public Condition newCondition() {
  return sync.newCondition();
}

final ConditionObject newCondition() {
  // ConditionObject 是 AQS 中對 Condition 的實現
  return new ConditionObject();
}

ConditionObject結構

上一篇文章中介紹了 Node 結構,這裡條件也使用了這個節點定義了一個單鏈表,統稱為條件佇列,上一篇介紹統稱同步佇列。條件佇列結構相當簡單就不單獨畫圖了。

// 條件佇列頭
private transient Node firstWaiter;
// 條件佇列尾
private transient Node lastWaiter;

// 因為預設感知中斷,需要考慮如何處理
// 退出條件佇列時重新設定中斷位
private static final int REINTERRUPT =  1;
// 退出條件佇列時直接拋異常
private static final int THROW_IE    = -1;

條件佇列入隊

  • AbstractQueuedSynchronizer.ConditionObject#await
public final void await() throws InterruptedException {
  if (Thread.interrupted())
    throw new InterruptedException();
  // 到條件佇列中排隊,下文詳解
  Node node = addConditionWaiter();
  // 此方法比較簡單,就是呼叫前一篇講過的 release 方法釋放鎖(呼叫 await 時必定是鎖的持有者)
  // savedState 是進入條件佇列前,持有鎖的數量
  // 失敗會直接丟擲異常,並且最終把節點狀態設定為 CANCELLED
  int savedState = fullyRelease(node);
  int interruptMode = 0;
  // 判斷在不在同步佇列(當呼叫signal之後會從條件佇列移到同步佇列),此判斷很簡單:節點狀態是 CONDITION 肯定 false,否則就到同步佇列中去找
  while (!isOnSyncQueue(node)) {
    // 掛起
    LockSupport.park(this);
    // 檢查是不是因為中斷被喚醒的,下文詳解
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
      break;
  }
  // 上一篇介紹過acquireQueued自旋搶鎖,如果搶到鎖了,並且中斷模式不是 -1(預設0),就記錄中斷模式為1,表示需要重新設定中斷
  if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    interruptMode = REINTERRUPT;
  // 清除條件佇列中取消的節點
  if (node.nextWaiter != null)
    // 下文詳解,在addConditionWaiter方法中也有用到
    unlinkCancelledWaiters();
  // 處理中斷
  if (interruptMode != 0)
    // 1:再次中斷	-1:丟擲異常
    reportInterruptAfterWait(interruptMode);
}
  • AbstractQueuedSynchronizer.ConditionObject#addConditionWaiter

加入條件佇列

private Node addConditionWaiter() {
  Node t = lastWaiter;
  // 如果條件佇列最後一個節點取消了,就清理
  if (t != null && t.waitStatus != Node.CONDITION) {
    unlinkCancelledWaiters();
    t = lastWaiter;
  }
  // 新建一個 waitStatus = -2 的節點
  Node node = new Node(Thread.currentThread(), Node.CONDITION);
  // 下面是簡單的單鏈表操作,之前同步佇列入隊用的 CAS 操作,因為會有很多執行緒去搶鎖,而執行緒進入條件佇列一定是拿到鎖了,不滿足條件了,所以不存在併發問題
  if (t == null)
    firstWaiter = node;
  else
    t.nextWaiter = node;
  lastWaiter = node;
  return node;
}
  • AbstractQueuedSynchronizer.ConditionObject#unlinkCancelledWaiters
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    // 輔助變數,用於接尾巴,trail始終等於迴圈中當前節點t的上一個不是取消狀態的節點
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        // 判斷當前節點有沒有取消
        if (t.waitStatus != Node.CONDITION) {
            // 斷當前節點鏈
            t.nextWaiter = null;
            // trail == null 說明目前條件佇列裡面全取消了
            if (trail == null)
                // 頭節點指向第一個沒取消的節點
                firstWaiter = next;
            else
                // trail 是 t 的前一個節點,也就是踢出了 t
                trail.nextWaiter = next;
            // 如果最後一個節點取消了,那需要改一下尾指標
            if (next == null)
                lastWaiter = trail;
        }
        else
            trail = t;
        t = next;
    }
}
  • AbstractQueuedSynchronizer.ConditionObject#checkInterruptWhileWaiting

上文 await 方法中,執行緒一旦喚醒會先檢查中斷

private int checkInterruptWhileWaiting(Node node) {
    // 沒中斷,返回0,中斷了需要放回同步佇列
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
    0;
}
  • AbstractQueuedSynchronizer#transferAfterCancelledWait
// 如果
final boolean transferAfterCancelledWait(Node node) {
    // 把因為中斷醒來的節點,設定狀態為全新的節點,從條件佇列放入同步佇列
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        enq(node);
        return true;
    }
    // 上面改狀態為什麼要 CAS ? 如果中斷喚醒的同時被 signal 喚醒了,在 signal 入隊成功之前讓出cpu,但是不釋放鎖
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

條件隊列出隊

單個喚醒和喚醒所以掉的方法類似,看一個單個喚醒流程就可

  • AbstractQueuedSynchronizer.ConditionObject#signal
public final void signal() {
    // 如果持有鎖的執行緒不是當前執行緒就拋異常,也就是隻有獲得鎖的執行緒可以執行喚醒操作
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    // 通知條件佇列中的第一個節點,也就是等的最久的節點
    if (first != null)
        doSignal(first);
}
  • AbstractQueuedSynchronizer.ConditionObject#doSignal
private void doSignal(Node first) {
    do {
        // 把 first 斷鏈
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
        // 如果轉移到同步佇列失敗了,並且還有條件佇列不為空就喚醒下一個
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}
  • AbstractQueuedSynchronizer#transferForSignal
final boolean transferForSignal(Node node) {
    // 如果節點取消了,轉移失敗
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 這裡的 p 是 node 在同步佇列裡的前驅節點
    Node p = enq(node);
    int ws = p.waitStatus;
    // 看過上一篇文章應該有映像,只要是進同步佇列,都需要把前一個節點狀態設為 -1
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // 如果取消了,或者狀態設定失敗,喚醒後繼續掛起
        LockSupport.unpark(node.thread);
    return true;
}

最後按照慣例結合上面的案例,畫張圖總結下:

相關推薦

Java原始碼ReentrantLock2

前言 本文是 ReentrantLock 原始碼的第二篇,第一篇主要介紹了公平鎖非公平鎖正常的加鎖解鎖流程,雖然表達能力有限不知道有沒有講清楚,本著不太監的原則,本文填補下第一篇中挖的坑。 Java讀原始碼之ReentrantLock 原始碼分析 感知中斷鎖 如果我們希望檢測到中斷後能立刻丟擲異常就用 loc

Collection原始碼2——LinkedList

上篇文章Collection原始碼之路(1)——ArrayList我們提到ArrayList其實就是可變長的陣列,我們都知道資料物理結構包括順序儲存和鏈式儲存兩種,特點是 順序儲存方便查詢,修改比較麻煩 鏈式儲存修改方便,查詢比較麻煩 ArrayList採用陣列的

Java原始碼ReentrantLock

## 前言 ReentrantLock 可重入鎖,應該是除了 synchronized 關鍵字外用的最多的執行緒同步手段了,雖然JVM維護者瘋狂優化 synchronized 使其已經擁有了很好的效能。但 ReentrantLock 仍有其存在價值,例如可以感知執行緒中斷,公平鎖模式,可以指定超時時間的搶鎖

Java核心技術》第10版讀書筆記Chap52——方法呼叫過程、final、型別轉換、abstract與訪問識別符號

方法呼叫過程 假設在原始碼中有這樣一行: manager.setBonus(2300); 下面來看看javac編譯器是如何處理的: 檢查根據物件型別和函式名稱,在該類成員方法及其父類中有呼叫權的成員方法中尋找到所有名字匹配的方法。在本例中,mana

java學習代理2:靜態代理和動態代理

一,代理的概念 代理是一個物件,代理物件為其他物件提供一種代理,以控制對這個物件的訪問,代理物件起到中介作用,可以去掉或者增加額外的服務。 如:火車票代售點就是火車站售票處的一個代理物件,可通過訪問代售點進行業務處理。 二,靜態代理的2種實現方式:繼承和聚合 靜態代理中的代

我的android多執行緒程式設計2RxJava Schedulers原始碼分析

寫在伊始 上一篇介紹了執行緒的一些基礎知識和工作這麼久以後對於多執行緒部分的使用經驗之路,這篇主要對RxJava執行緒控制部分進行分析。 RxJava(本文就RxJava2.0分析) 說實話,近一年多一直在用rxjava進行專案架構的編寫及封裝及一些非

Redis 數據結構dict2

value ash 每次 earch 定義 索引 user popu adding 本文及後續文章,Redis版本均是v3.2.8 上篇文章《Redis 數據結構之dict》,我們對dict的結構有了大致的印象。此篇文章對dict是如何維護數據結構的做個詳細的理解

HDFS源碼分析NameNode2————Format

return exceptio 數據信息 row oid creat tail 進行 alt    在Hadoop的HDFS部署好了之後並不能馬上使用,而是先要對配置的文件系統進行格式化。在這裏要註意兩個概念,一個是文件系統,此時的文件系統在物理上還不存在,或許是網絡磁盤來

.6-Vue源碼AST2

png 變量聲明 enc 標簽 ons directive option 復雜 html   上一節獲取到了DOM樹的字符串,準備進入compile階段: // Line-9326 function compileToFunctions(template,

Java集合復習Collection2添加一組元素

ray addall 調整 supported cnblogs rust 數組 dal sta 1 import java.util.*; 2 class Snow{} 3 class Powder extends Snow{} 4 class Crusty ex

python學習_day54_前端基礎js2

data 截取 定義 得到 let 結果 是什麽 index 插入   在JavaScript中除了null和undefined以外其他的數據類型都被定義成了對象,也可以用創建對象的方法定義變量,String、Math、Array、Date、RegExp都是JavaScri

angular4爬坑2angular腳手引入第三方類庫

scrip 1-1 sta 引入 logs .json 類比 ima 第三方 如何在angular4腳手架中引入第三方類庫呢比如jquery、swiper、bootstrap。。。。。。 例如引入jquery:(其他類庫類比jquery即可) 第一步:在我們的項目目錄下

USB小白學習2端點IN/OUT互換

speed 9.png 現在 script des 裏的 宋體 dir info 端點2(out)和端點6(in)的out_in互換 註:這裏的out和in都是以host為標準說的,out是host的out,在設備(Cy7c68013)這裏其實是輸入端口;in是host的i

STL源碼剖析allocator2

內部 擁有 struct trait 多個 接受 rst stl -m SGI雖然定義了名為allocator的配置器,但從未使用過。SGI的allocator只是包裝了C++的::operatpor new和::operator delete,效率不高。STL中內存配置操

單片機小白的啟程2

數據 物聯網 分析 無線網 bsp 工作 信息 pm2 指令 單片機與物聯網的關系: 物聯網的三大部分:感知層、傳輸層、應用層。          感知層的主要器件是傳感器,包括光傳感器、PM2.5傳感器、電容觸摸傳感器等。其主要作用是使傳感器收集信息,而感知層收集到信息後

Python學習2——Python種類介紹

VM 種類 廣泛 分享 java字節碼 流程 字節碼 基礎上 python Python的種類 Cpython Python的官方版本,使用C語言實現,使用最為廣泛,CPython實現會將源文件(py文件)轉換成字節碼文件(pyc文件),然後運行在Python虛擬

java虛擬機GC

reg 建立 行高 resize 組合 完成 player 操作 出棧 垃圾回收主要內容:   1. 那些內存需要回收?   2. 什麽時候回收?   3. 如何回收? 垃圾回收主要針對運行時數據區那些區域?   運行時數據區的線程私有區域有:虛擬機棧,本地方法棧,程序計數

JAVA基礎學習

個人理解 整形 之路 boolean mman 類型 布爾 屬性 註釋 JAVA基礎概念: PATHl: path屬於操作系統的屬性,是系統用來搜尋可執行文件的路徑,個人理解是類似於linux中的全局變量 CALSSPATH:java程序解釋類文件時加載文件的路徑 註釋

JAVA基礎學習數組的定義

對象 ava void 數據類型 class 語法 info int .com 什麽是數組:就是一堆相同類型的數據放一堆(一組相關變量的集合) 定義語法: 聲明並開辟數組     數據類型 數組名[] = new 數據類型[長度]; 分布完成 聲明數組:數據類型

JAVA基礎學習數組與方法參數的傳遞

就是 .com 另一個 AS oid span 參數 spa nbsp 通常,向方法中傳遞的都是基本數據類型,而向方法中傳遞數組時,就需要考慮內存的分配 public class test2 { public static void main(String a