java原始碼-ReentrantLock之NonfairSync

晴天哥發表於2018-08-30

開篇

 NonfairSync和FairSync相比而言,多了一次搶佔機會,其他處理邏輯幾乎是一模一樣。

加鎖過程

ReentrantLock的的鎖過程如下:

  • 1、先嚐試獲取鎖,通過tryAcquire()實現。
  • 2、獲取鎖失敗後,執行緒被包裝成Node物件後新增到CLH佇列,通過addWaiter()實現。
  • 3、新增CLH佇列後,逐步的去執行CLH佇列的執行緒,如果當前執行緒獲取到了鎖,則返回;否則,當前執行緒進行休眠,直到喚醒並重新獲取鎖了才返回。
    public void lock() {
        sync.lock();
    }

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

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

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

acquire的操作流程

  • 1、第一步通過tryAcquire()嘗試獲取鎖,成功則返回
  • 2、獲取鎖失敗後通過addWaiter新增到CLH佇列的末尾
  • 3、新增CLH佇列後,通過acquireQueued()方法逐步的去執行CLH佇列的執行緒,如果當前執行緒獲取到了鎖則返回;否則當前執行緒進行休眠,直到喚醒並重新獲取鎖後返回。
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

tryAcquire的操作流程

  • 1、如果鎖未佔用的情況下:當前執行緒直接搶佔鎖並設定鎖佔用執行緒為當前執行緒,非公平鎖NonfairSync和FairSync的差別就在於這個地方,非公平鎖直接搶佔鎖,而公平鎖則需要判斷是否位於頭結點來決定是否搶佔。
  • 2、如果鎖被佔用的情況下:判斷當前執行緒是否是佔用鎖執行緒,如果是則實現鎖的可重入功能,設定鎖佔用次數。
  • 3、如果上述全否那麼就返回佔鎖失敗的。
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }

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

addWaiter的操作流程

  • 1、將當前執行緒包裝成Node物件。
  • 2、先嚐試通過快速失敗法嘗試在CLH隊尾插入Node物件
  • 3、如果快速插入失敗後那麼就通過enq方法在CLH隊尾插入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;
    }

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

acquireQueued的操作流程

  • 1、如果當前節點Node的前驅節點屬於head,當前節點屬於老二地位通過tryAcquire()嘗試獲取鎖,獲取成功後那麼就釋放原head節點(可以理解為head已經釋放鎖然後從CLH刪除),把當前節點設定為head節點。
  • 2、通過shouldParkAfterFailedAcquire()方法判斷Node代表的執行緒是否進入waiting狀態,直到被unpark()。
  • 3、parkAndCheckInterrupt()方法將當前執行緒進入waiting狀態。
  • 4、休眠執行緒被喚醒的時候會執行 if (p == head && tryAcquire(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)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire的操作流程

  • 1、如果前置節點處於SIGNAL狀態,那麼當前執行緒進入阻塞狀態,返回true
  • 2、如果前置節點處於ws>0也就是取消狀態,那麼當前執行緒節點就往前查詢第一個狀態處於ws<=0的節點
  • 3、如果前置狀態ws=0的節點,那麼就把前置節點設定為SIGNAL狀態
  • 4、整個shouldParkAfterFailedAcquire函式是在for()迴圈當中迴圈執行的,我們可以想象按照步驟2->3->1的順序執行,按照前置遍歷尋找合適的前置節點,接著發現前置節點ws狀態為0後重新設定為SIGNAL,最後發現前置節點狀態為SINGAL後休眠執行緒自身。
  • 5、執行緒從執行態進入waiting狀態其實也是經歷了一系列的處理過程的。
    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;
    }

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

解鎖過程

release過程

  • 1、通過tryRelease()方法嘗試讓當前執行緒釋放鎖物件
  • 2、通過unparkSuccessor()方法設定當前節點狀態ws=0並且喚醒CLH佇列中的下一個等待執行緒
    public void unlock() {
        sync.release(1);
    }

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

tryRelease過程

  • 1、如果佔用鎖執行緒非當前執行緒直接拋異常
  • 2、遞減鎖計數後如果值為0那麼就釋放當前鎖佔用者
  • 3、更新鎖狀態為未佔用,即state為0
    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;
    }

unparkSuccessor過程

  • 1、設定當前Node狀態為0
  • 2、尋找下一個等待執行緒節點來喚醒等待執行緒並通過LockSupport.unpark()喚醒執行緒
  • 3、尋找下一個等待執行緒,如果當前Node的下一個節點符合狀態就直接進行喚醒,否則從隊尾開始進行倒序查詢,找到最優先的執行緒進行喚醒。
    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);
    }


相關文章