[懷舊并發06]分析ReentrantLock的實現原理

Java并發編程源碼分析系列:

前幾篇文章分析了線程池的原理,接下來研究鎖的方面。顯式鎖ReentrantLock和同步工具類的實現基礎都是AQS,所以合起來一齊研究。

什么是AQS

AQS即是AbstractQueuedSynchronizer,一個用來構建鎖和同步工具的框架,包括常用的ReentrantLock、CountDownLatch、Semaphore等。

AQS沒有鎖之類的概念,它有個state變量,是個int類型,在不同場合有著不同含義。本文研究的是鎖,為了好理解,姑且先把state當成鎖。

AQS圍繞state提供兩種基本操作“獲取”和“釋放”,有條雙向隊列存放阻塞的等待線程,并提供一系列判斷和處理方法,簡單說幾點:

  • state是獨占的,還是共享的;
  • state被獲取后,其他線程需要等待;
  • state被釋放后,喚醒等待線程;
  • 線程等不及時,如何退出等待。

至于線程是否可以獲得state,如何釋放state,就不是AQS關心的了,要由子類具體實現。

直接分析AQS的代碼會比較難明白,所以結合子類ReentrantLock來分析。AQS的功能可以分為獨占和共享,ReentrantLock實現了獨占功能,是本文分析的目標。

ReentrantLock對比synchronized

Lock lock = new ReentranLock();
lock.lock();
try{
    //do something
}finally{
    lock.unlock();
}

ReentrantLock實現了Lock接口,加鎖和解鎖都需要顯式寫出,注意一定要在適當時候unlock。

和synchronized相比,ReentrantLock用起來會復雜一些。在基本的加鎖和解鎖上,兩者是一樣的,所以無特殊情況下,推薦使用synchronized。ReentrantLock的優勢在于它更靈活、更強大,除了常規的lock()、unlock()之外,還有lockInterruptibly()、tryLock()方法,支持中斷、超時。

公平鎖和非公平鎖

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock的內部類Sync繼承了AQS,分為公平鎖FairSync和非公平鎖NonfairSync。

  • 公平鎖:線程獲取鎖的順序和調用lock的順序一樣,FIFO;
  • 非公平鎖:線程獲取鎖的順序和調用lock的順序無關,全憑運氣。

ReentrantLock默認使用非公平鎖是基于性能考慮,公平鎖為了保證線程規規矩矩地排隊,需要增加阻塞和喚醒的時間開銷。如果直接插隊獲取非公平鎖,跳過了對隊列的處理,速度會更快。

嘗試獲取鎖

final void lock() { acquire(1);}

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

先來看公平鎖的實現,lock方法很簡單的一句話調用AQS的acquire方法:

protected boolean tryAcquire(int arg) {    
        throw new UnsupportedOperationException();
}

噢,AQS的tryAcquire不能直接調用,因為是否獲取鎖成功是由子類決定的,直接看ReentrantLock的tryAcquire的實現。

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

獲取鎖成功分為兩種情況,第一個if判斷AQS的state是否等于0,表示鎖沒有人占有。接著,hasQueuedPredecessors判斷隊列是否有排在前面的線程在等待鎖,沒有的話調用compareAndSetState使用cas的方式修改state,傳入的acquires寫死是1。最后線程獲取鎖成功,setExclusiveOwnerThread將線程記錄為獨占鎖的線程。

第二個if判斷當前線程是否為獨占鎖的線程,因為ReentrantLock是可重入的,線程可以不停地lock來增加state的值,對應地需要unlock來解鎖,直到state為零。

如果最后獲取鎖失敗,下一步需要將線程加入到等待隊列。

線程進入等待隊列

AQS內部有一條雙向的隊列存放等待線程,節點是Node對象。每個Node維護了線程、前后Node的指針和等待狀態等參數。

線程在加入隊列之前,需要包裝進Node,調用方法是addWaiter:

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;
}

每個Node需要標記是獨占的還是共享的,由傳入的mode決定,ReentrantLock自然是使用獨占模式Node.EXCLUSIVE。

創建好Node后,如果隊列不為空,使用cas的方式將Node加入到隊列尾。注意,這里只執行了一次修改操作,并且可能因為并發的原因失敗。因此修改失敗的情況和隊列為空的情況,需要進入enq。

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

enq是個死循環,保證Node一定能插入隊列。注意到,當隊列為空時,會先為頭節點創建一個空的Node,因為頭節點代表獲取了鎖的線程,現在還沒有,所以先空著。

阻塞等待線程

線程加入隊列后,下一步是調用acquireQueued阻塞線程。

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

標記1是線程喚醒后嘗試獲取鎖的過程。如果前一個節點正好是head,表示自己排在第一位,可以馬上調用tryAcquire嘗試。如果獲取成功就簡單了,直接修改自己為head。這步是實現公平鎖的核心,保證釋放鎖時,由下個排隊線程獲取鎖。(看到線程解鎖時,再看回這里啦)

標記2是線程獲取鎖失敗的處理。這個時候,線程可能等著下一次獲取,也可能不想要了,Node變量waitState描述了線程的等待狀態,一共四種情況:

static final int CANCELLED =  1;   //取消
static final int SIGNAL    = -1;     //下個節點需要被喚醒
static final int CONDITION = -2;  //線程在等待條件觸發
static final int PROPAGATE = -3; //(共享鎖)狀態需要向后傳播

shouldParkAfterFailedAcquire傳入當前節點和前節點,根據前節點的狀態,判斷線程是否需要阻塞。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  int ws = pred.waitStatus;
  if (ws == Node.SIGNAL)
      return true;
  if (ws > 0) {
      do {
          node.prev = pred = pred.prev;
      } while (pred.waitStatus > 0);
      pred.next = node;
  } else {
      compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  }
  return false;
}
  • 前節點狀態是SIGNAL時,當前線程需要阻塞;
  • 前節點狀態是CANCELLED時,通過循環將當前節點之前所有取消狀態的節點移出隊列;
  • 前節點狀態是其他狀態時,需要設置前節點為SIGNAL。

如果線程需要阻塞,由parkAndCheckInterrupt方法進行操作。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

parkAndCheckInterrupt使用了LockSupport,和cas一樣,最終使用UNSAFE調用Native方法實現線程阻塞(以后有機會就分析下LockSupport的原理,park和unpark方法作用類似于wait和notify)。最后返回線程喚醒后的中斷狀態,關于中斷,后文會分析。

到這里總結一下獲取鎖的過程:線程去競爭一個鎖,可能成功也可能失敗。成功就直接持有資源,不需要進入隊列;失敗的話進入隊列阻塞,等待喚醒后再嘗試競爭鎖。

釋放鎖

通過上面詳細的獲取鎖過程分析,釋放鎖過程大概可以猜到:頭節點是獲取鎖的線程,先移出隊列,再通知后面的節點獲取鎖。

public void unlock() {
    sync.release(1);
}

ReentrantLock的unlock方法很簡單地調用了AQS的release:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

和lock的tryAcquire一樣,unlock的tryRelease同樣由ReentrantLock實現:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

因為鎖是可以重入的,所以每次lock會讓state加1,對應地每次unlock要讓state減1,直到為0時將獨占線程變量設置為空,返回標記是否徹底釋放鎖。

最后,調用unparkSuccessor將頭節點的下個節點喚醒:

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);
}

尋找下個待喚醒的線程是從隊列尾向前查詢的,找到線程后調用LockSupport的unpark方法喚醒線程。被喚醒的線程重新執行acquireQueued里的循環,就是上文關于acquireQueued標記1部分,線程重新嘗試獲取鎖。

中斷鎖

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

在acquire里還有最后一句代碼調用了selfInterrupt,功能很簡單,對當前線程產生一個中斷請求。

為什么要這樣操作呢?因為LockSupport.park阻塞線程后,有兩種可能被喚醒。

第一種情況,前節點是頭節點,釋放鎖后,會調用LockSupport.unpark喚醒當前線程。整個過程沒有涉及到中斷,最終acquireQueued返回false時,不需要調用selfInterrupt。

第二種情況,LockSupport.park支持響應中斷請求,能夠被其他線程通過interrupt()喚醒。但這種喚醒并沒有用,因為線程前面可能還有等待線程,在acquireQueued的循環里,線程會再次被阻塞。parkAndCheckInterrupt返回的是Thread.interrupted(),不僅返回中斷狀態,還會清除中斷狀態,保證阻塞線程忽略中斷。最終acquireQueued返回true時,真正的中斷狀態已經被清除,需要調用selfInterrupt維持中斷狀態。

因此普通的lock方法并不能被其他線程中斷,ReentrantLock是可以支持中斷,需要使用lockInterruptibly。

兩者的邏輯基本一樣,不同之處是parkAndCheckInterrupt返回true時,lockInterruptibly直接throw new InterruptedException()。

非公平鎖

分析完公平鎖的實現,還剩下非公平鎖,主要區別是獲取鎖的過程不同。

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

在NonfairSync的lock方法里,第一步直接嘗試將state修改為1,很明顯,這是搶先獲取鎖的過程。如果修改state失敗,則和公平鎖一樣,調用acquire。

final boolean nonfairTryAcquire(int acquires) {
  final Thread current = Thread.currentThread();
  int c = getState();
  if (c == 0) {
      if (compareAndSetState(0, acquires)) {
          setExclusiveOwnerThread(current);
          return true;
      }
  }
  else if (current == getExclusiveOwnerThread()) {
      int nextc = c + acquires;
      if (nextc < 0) // overflow
          throw new Error("Maximum lock count exceeded");
      setState(nextc);
      return true;
  }
  return false;
}

nonfairTryAcquire和tryAcquire乍一看幾乎一樣,差異只是缺少調用hasQueuedPredecessors。這點體驗出公平鎖和非公平鎖的不同,公平鎖會關注隊列里排隊的情況,老老實實按照FIFO的次序;非公平鎖只要有機會就搶占,才不管排隊的事。

總結

從ReentrantLock的實現完整分析了AQS的獨占功能,總的來講并不復雜。別忘了AQS還有共享功能,下一篇是--分析CountDownLatch的實現原理

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容