【Java併發】-- ReentrantLock 可重入鎖實現原理1 - 獲取鎖


Lock有很多具體的鎖的實現,但最直觀的實現是ReentrantLock重入鎖,也是平時我們用的最多的。重入鎖是獨佔鎖的代表。

文章中的所有源碼全部來源於jdk1.8

ReentrantLock 重入鎖

表示可重入的鎖,舉個例子:當線程t1通過調用lock()方法獲取鎖之後,再次調用lock,是不會再阻塞獲取鎖的,直接增加重試次數就行了。synchronized 和 ReentrantLock 都是可重入鎖;
來個簡單的demo來說明一下重入性:

public class ReentrantDemo{
	public synchronized void demo(){
		System.out.println("begin:demo");
		demo2();
	}
	public void demo2(){
		System.out.println("begin:demo1");
		synchronized (this){
		}
}

public static void main(String[] args) {
	ReentrantDemo rd=new ReentrantDemo();
	new Thread(rd::demo).start();
}

main裏面啓動了一個線程,調用synchronized修飾的demo方法,獲得了當前的對象鎖,然後demo中調用了demo2方法,demo2中又存在同一個市裏說,此時,當前線程會因爲無法持有demo2的對象鎖而阻塞,這時就會發生死鎖。重入鎖的目的是爲了避免此種死鎖問題。

重入鎖的類圖:

在這裏插入圖片描述


ReentrantLock 核心方法

void lock()    // 如果鎖可用就獲得鎖,如果鎖不可用就阻塞直到鎖釋放
void lockInterruptibly()  // 和lock()方法相似, 但阻塞的線程可中斷 , 拋出java.lang.InterruptedException 異常
boolean tryLock()    // 非阻塞獲取鎖;嘗試獲取鎖,如果成功返回 true
boolean tryLock(long timeout, TimeUnit timeUnit) //帶有超時時間的獲取鎖方法
void unlock()    // 釋放鎖

當然,這些核心方法內部還是基於AQS來實現的

ReentrantLock 源碼分析

        ReentrantLock lock = new ReentrantLock();        
        lock.lock();
        try {
            System.out.println("-------todo---------");
        } finally {
            lock.unlock();   // 放在finally 裏面是爲了保證鎖最終一定能被釋放
        }

這裏我們以lock()爲切入點,看AQS同步隊列的實現過程;

時序圖

先來一張時序圖看看整體的流程:
在這裏插入圖片描述

1. ReentrantLock.lock()

這裏是入口了,獲取鎖的入口;sync是ReentrantLock的抽象的靜態內部類,它繼承AQS來實現重入鎖的邏輯,這個系列博客的第一篇文章介紹了AQS,AQS 是一個同步隊列,它能夠實現線程的阻塞以及喚醒, 但它並不具備業務功能, 所以在不同的同步場景中,不同的鎖會繼承 AQS 來實現對應場景的功能;

    public void lock() {
        sync.lock();
    }

Sync 有兩個具體的實現類,分別是:
NofairSync: 表示可以存在搶佔鎖的功能,也就是說不管當前隊列上是否存在其他線程等待,新線程都有機會搶佔鎖
FailSync: 表示所有線程嚴格按照 FIFO 來獲取鎖,需要判斷是否有前驅節點;
默認走的是非公平鎖的邏輯,我們以非公平鎖爲例,看一下lock()中的實現

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
  1. 非公平鎖和公平鎖最大的區別在於,在非公平鎖中我搶佔鎖的邏輯是,不管有沒有線程排隊,我先上來 cas 去搶佔一下
  2. CAS 成功,就表示成功獲得了鎖
  3. CAS 失敗,調用 acquire(1)走鎖競爭邏輯
    在公平鎖中,是沒有這個if判斷的,直接走 acquire(1)的邏輯;
    下面我們看accquire()的邏輯

2. AQS.accquire()

acquire 是 AQS 中的核心方法,如果 CAS 操作未能成功,已經有別的線程搶到了鎖,說明 state 已經不爲 0(0是無鎖時候的初始態),此時繼續 acquire(1)操作;

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

在第一篇文章AQS裏面我們也介紹過這個獨佔鎖的主要邏輯,這裏我們再詳細分析一下具體實現;

  1. 通過 tryAcquire 嘗試獲取獨佔鎖,如果成功返回 true,失敗返回 false
  2. 如果 tryAcquire 失敗,則會通過 addWaiter 方法將當前線程封裝成 Node 添加
    到 AQS 隊列尾部
  3. acquireQueued,將 Node 作爲參數,通過自旋去嘗試獲取鎖。
    下面每個方法逐一分析:

3. NonfairSync.tryAcquire(arg)

AQS 中 tryAcquire 方法的定義,並沒有實現,而是拋出異常。這裏都留給子類做具體實現了。

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

nonfairTryAcquire纔是真正實現tryAcquire邏輯的方法:

       /*
       * 非公平鎖實現嘗試獲取獨佔鎖
       */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();  // 獲取當前執行的線程
            int c = getState();    // 獲取state的值
            if (c == 0) {     // 無鎖狀態時(初始態),這裏再cas一次試試看能不能搶到
          		//cas 替換 state 的值, cas 成功表示獲取鎖成功
                if (compareAndSetState(0, acquires)) { 
                //保存當前獲得鎖的線程,下次再來的時候不需要再嘗試競爭鎖,直接走後續重入即可:state+1
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
            //如果同一個線程來獲得鎖,直接增加重入次數 ,state的值遞增
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;    // 獲取鎖成功
            }
            return false;   // 獲取鎖失敗
        }

4.AQS.addWaiter()

若tryAcquire 方法獲取鎖成功則執行任務,然後再等着釋放即可,我們這裏先分析獲取鎖失敗以後線程阻塞態的邏輯。
獲取鎖失敗則會先調用 addWaiter 將當前線程封裝成Node,入參 mode 表示當前節點的狀態,傳遞的參數是 Node.EXCLUSIVE,表示獨佔狀態。意味着重入鎖用到了 AQS 的獨佔鎖功能;EXCLUSIVE在ReentrantLock好幾處都有體現。addWaiter()的邏輯大致分爲以下幾個步驟:

  1. 將當前線程封裝成 Node
  2. 當前鏈表中的 tail 節點是否爲空,如果不爲空,則通過 cas 操作把當前線程的node 添加到 AQS 隊列
  3. 如果爲空或者 cas 失敗,調用 enq 將節點添加到 AQS 隊列
    來看一下addWaiter()和enq()的實現;

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;  // tail是AQS同步隊列的隊尾,默認爲null
        if (pred != null) { // tail不爲空,說明隊列存在節點
            node.prev = pred; // //把當前線程的 Node 的 prev 指向 tail
            if (compareAndSetTail(pred, node)) { // 通過 cas把node加入到 AQS 隊列,也就是設置爲 tail
                pred.next = node; // 設置成功後,把原tail節點的next指向當前node
                return node;
            }
        }
        enq(node);  //tail =null ,把node添加到同步隊列
        return node;
    }

enq()

enq 就是通過自旋操作把當前節點加入到隊列中

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node())) // 通過case初始化一個新的node,
                    tail = head; //初始化狀態時頭尾指向同一節點
            } else {  
                node.prev = t;
                if (compareAndSetTail(t, node)) {  // 初始化成功後通過cas將節點添加到隊尾
                    t.next = node;
                    return t;
                }
            }
        }
    }

圖解分析:
當三個線程同時來競爭鎖時,調用addwaiter方法結束後的邏輯:
ThreadA 獲得了鎖資源,ThreadB和ThreadC競爭鎖,然後排隊。
在這裏插入圖片描述

5. AQS.acquireQueued()

通過addwaiter()方法把線程添加到鏈表後,會接着把node做爲參數傳遞給acquireQueued()方法,去競爭鎖;結合上一個圖解,就是會把ThreadB 和ThreadC 封裝後的node節點分別傳給acquireQueued,去競爭ThreadA執行完任務後釋放的鎖;

acquireQueued的主要邏輯如下:

  1. 獲取當前節點的 prev 節點
  2. 如果 prev 節點爲 head 節點,那麼它就有資格去爭搶鎖,調用 tryAcquire 搶佔鎖
  3. 搶佔鎖成功以後,把獲得鎖的節點設置爲 head,並且移除原來的初始化 head節點
  4. 如果獲得鎖失敗,則根據 waitStatus 決定是否需要掛起線程
  5. 最後,通過 cancelAcquire 取消獲得鎖的操作
      /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); // 獲得當前節點的前驅節點賦給p
                if (p == head && tryAcquire(arg)) { // 若p爲head節點,則當前節點有資格去競爭鎖
                    setHead(node);   // 將當前節點設爲頭結點,也就是當前節點獲得鎖成功,成爲新的頭結點獲得執行權
                    p.next = null; // 把原頭結點斷開,從同步隊列中刪除
                    failed = false;
                    return interrupted;
                }
                // 若此時原頭結點ThreadA還沒有完全釋放鎖,則ThreadB/ThreadC就會在tryAcquire時候返回false,
                // 就會走後面的shouldParkAfterFailedAcquire方法,看下面的具體分析
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire

這個方法的主要作用是,通過 Node 的狀態來判斷, ThreadB 競爭鎖失敗以後是否應該被掛起;
先來回顧一下node的五個狀態:
Node 有 5 中狀態,分別是: CANCELLED(1), SIGNAL(-1)、 CONDITION(-2)、 PROPAGATE(-3)、默認狀態(0)

  • CANCELLED: 在同步隊列中等待的線程等待超時或被中斷,需要從同步隊列中取消該 Node 的結點, 其結點的 waitStatus 爲 CANCELLED,即結束狀態,進入該狀態後的結點將不會再變化
  • SIGNAL: 只要前置節點釋放鎖,就會通知標識爲 SIGNAL 狀態的後續節點的線程CONDITION: 和 Condition 有關係,後續會講解
  • PROPAGATE: 共享模式下, PROPAGATE 狀態的線程處於可運行狀態
  • CONDITION: 只有在等待隊列纔會有的狀態,同步隊列不會出現此狀態
  • 0: 初始狀態

ThreadB 競爭鎖失敗以後是否應該被掛起??

  1. 如果 ThreadB 的 pred 節點狀態爲 SIGNAL,那就表示可以放心掛起當前線程
  2. 通過循環掃描鏈表把 CANCELLED 狀態的節點移除 (去掉無效的節點)
  3. 修改 pred 節點的狀態爲 SIGNAL,返回 false

返回 false 時,也就是不需要掛起,返回 true,則需要調用 parkAndCheckInterrupt掛起當前線程

    /**
     * Checks and updates status for a node that failed to acquire.
     * Returns true if thread should block. This is the main signal
     * control in all acquire loops.  Requires that pred == node.prev.
     *
     * @param pred node's predecessor holding status
     * @param node the node
     * @return {@code true} if thread should block
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;   // 獲取前驅節點的狀態
        if (ws == Node.SIGNAL)  // 前驅節點會singal時候,直接返回true,掛起當前節點
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;   
        if (ws > 0) {// 狀態大於0表示無效節點(只有處於cancel時大於0),取消了排隊,需要移除該節點
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;  
            } while (pred.waitStatus > 0);   // 從鏈表中移除該cancel態的節點,用循環保證移除成功
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
             // 利用 cas 設置 prev 節點的狀態爲 SIGNAL(-1)
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

到這一步,若ThreadA還沒有釋放鎖,ThreadB和ThreadC只能被掛起(標紅的表示掛起)
在這裏插入圖片描述


釋放鎖的過程下篇再分析吧,有點累了額~~
(。・ˇдˇ・。)

傳送門在這裏,看看釋放鎖的邏輯
【Java併發】-- ReentrantLock 可重入鎖實現原理2 - 釋放鎖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章