Java多線程系列之JUC鎖 - ReentrantLock

1、ReentrantLock的介紹

    ReentrantLock在源碼中的解釋是做爲可重入互斥鎖與synchronized有基本相同的行爲和語義可是它在此基礎上又擴展了其餘的一些功能。ReentrantLock鎖的線程持有者是上一個成功加鎖且不曾釋放的線程。ReentrantLock 是獨佔鎖在同一時間只能被一個線程持有,它做爲可重入鎖表如今它可被單個線程屢次持有,源碼註釋中提到當一個線程調用lock方法的時候,若是當前持有鎖的是該線程自己,將當即成功獲取鎖,能夠經過isHeldByCurrentThread或getHoldCount確認。java

    ReentrantLock分爲公平鎖和非公平鎖,可經過在建立實例時在構造函數中指定可選是否公平參數fairness肯定使用公平鎖仍是非公平鎖,若未指定則默認使用非公平鎖。公平鎖與非公平鎖的區別在於獲取鎖的順序,公平鎖保證等待時間最長的線程有限獲取鎖而非公平鎖不保證線程訪問獲取鎖的任何順序,主要是經過一個FIFO等待隊列實現的,若爲公平鎖則線程一次排隊獲取鎖,每次由隊列頭優先獲取鎖,非公平鎖模式下不管等待線程是否位於隊列頭部只要鎖釋放都有同等機會競爭鎖。在多線程環境下使用公平鎖的吞吐量可能會低於使用默認的非公平鎖,可是公平鎖能夠保證了線程獲取鎖更小的時間差別,且有效防止了線程飢餓(某個線程每次CPU執行機會都被其餘線程搶佔致使飢餓致死)。注意公平鎖僅僅保證獲取鎖的公平性但不保證線程的調度順序,使用公平鎖的多個線程中的某個線程可能得到比其餘線程更多的成功執行機會,前提是其餘線程未獲取到該鎖且其餘活躍線程未被處理,這很容易理解咱們在程序中公平鎖只能保證最久等待的線程優先獲取鎖可是對於線程調度這是由操做系統控制的。不管是公平鎖仍是非公平鎖,tryLock 方法並無使用公平設置。即便其餘線程正在等待,只要該鎖是可用的,此方法就能夠得到成功。node

    推薦在使用lock方法鎖定的同步代碼塊中使用try..finally包裹在finally中使用unlock釋放鎖。ReentrantLock在反序列時必定是解鎖狀態不管它在以前的序列化時是不是加鎖狀態,此外雖然說ReentrantLock對單一線程可重入,可是同一個線程最多2147483647的遞歸加鎖限制,若超出這個限制會在加鎖方法報錯算法

2、ReentrantLock數據結構

public class ReentrantLock implements Lock, java.io.Serializable {
    // 同步器,Sync是AbstractQueuedSynchronizer的派生類,ReentrantLock實現鎖主要經過該對象實現
    private final Sync sync;

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

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

}

    ReentrantLock實現了Lock類,Lock定義了相對synchronized更靈活且更多的鎖操做,實現了Serializable支持序列化。構造方法包括默認構造方法和一個帶參構造方法,默認構造方法建立的是非公平鎖,帶參構造方法ReentrantLock(boolean fair)基於fair建立公平鎖或者非公平鎖。底層也是基於AQS(AbstractQueuedSynchronizer)實現的,ReentranLock的核心是內部成員對象sync,所屬類Sync是ReentrantLock內部定義的AQS派生類,在ReentranLock有兩個實現類FairSync和FairSync分別對應非公平鎖和公平鎖。設計模式

3、ReenTrantLock源碼解析

1 - void lock()方法 - 獲取鎖

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

    咱們先來看下ReentrantLock非公平鎖該方法的實現,進入NonfairSync類lock方法數據結構

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

    邏輯較爲簡單,首先基於CAS算法調用compareAndSetState嘗試獲取鎖,若鎖未被持有即鎖同步狀態state=0那麼它將嘗試更新鎖狀態state=1表示鎖已經被持有,這裏鎖的同步狀態state還用於保存鎖持有線程獲取鎖操做的次數,這裏使用compareAndSet原子更新state的值是爲了確保state在上一次檢查以後該狀態未發生變動,更新鎖同步狀態state成功以後調用setExclusiveOwnerThread方法設置鎖持有線程exclusiveOwnerThread爲當前線程,也是AQS內部成員變量,用於區分多線程獲取鎖操做時重入鎖仍是競爭鎖。咱們看下該方法源碼:多線程

protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    就是更新成員變量exclusiveOwnerThread爲當前線程對象,若獲取鎖失敗則調用acquire方法嘗試獲取鎖,進入該方法(方法源碼位於AbstractQueuedSynchronizer)app

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

    acquire方法首先經過tryAcquire方法嘗試獲取鎖,若獲取鎖成功則直接返回不然會先經過addWaiter方法將當前線程加入CLH鎖等待隊列末尾而後調用acquireQueued方法,等待前面的線程執行並釋放鎖以後獲取鎖,若在休眠過程當中被中斷過則調用selfIntegerrupt方法本身產生一箇中斷。tryAcquire方法公平鎖和非公平鎖的實現不一樣,咱們進入NofairSync類內部看下該方法的實現函數

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

    由NonfairSync的內部實現源碼可知,非公平鎖中線程嘗試獲取鎖並無用到CLH等待隊列,相反只要鎖空閒(state=0)就能夠直接獲取鎖而且NonfairSync的非公平鎖也支持單線程重入。ui

    若獲取鎖失敗首先調用addWaiter方法,該方法是在AQS實現的,進入該方法源碼看下作了什麼this

private Node addWaiter(Node mode) {
        // 爲當前線程新建一個Node節點,節點的模型是前面傳過來的獨佔鎖模型
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        // 若CLH隊列不爲空,則將當前線程節點添加到CLH隊列末尾
        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;
                }
            }
        }
    }

    很簡單addWaiter方法就是判斷CLH等待隊列是否爲空,若爲空則新建一個CLH表頭;而後將當前線程節點添加到CLH末尾。不然,直接將當前線程節點添加到CLH等待隊列末尾,添加線程到等待隊列以後接下來看下acquireQueued方法,進入該方法源碼

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            // interrupt標識在CLH等待隊列的調度中當前線程在休眠時有沒有被中斷過
            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;
                }
                // 線程若應該阻塞則調用park阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                //放棄獲取鎖
                cancelAcquire(node);
        }
    }

    簡單來講acquireQueued方法的做用是逐步的去執行CLH等待隊列的線程,若是當前線程獲取到了鎖,則返回;不然,當前線程進行休眠,直到喚醒並從新獲取鎖了才返回

    下面順便進入selfInterrupt()方法源碼看下它做了啥

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

    看來就是調用當前線程對象的interrupt方法作了中斷。

    總結ReentrantLock的非公平鎖的獲取鎖方法lock的基本邏輯以下:

1)首先嚐試獲取鎖,若鎖處於空閒狀態,獲取鎖成功,直接返回;

2)若鎖已被佔用且是當前線程,ReentrantLock支持鎖重入,若是鎖同步狀態state(或者稱之爲當前鎖持有線程遞歸加鎖次數)超過int的最大值則拋出異常,不然加鎖成功,更新state;

3)若鎖已被佔用且持有鎖的不是當前線程則將當前線程加入CLH鎖等待隊列末尾,等待前面的線程執行並釋放鎖以後獲取鎖,若在休眠過程當中被中斷過則調用selfIntegerrupt方法本身產生一箇中斷。

    接下來 咱們先來看下ReentrantLock公平鎖該方法的實現,進入FairSync類lock方法

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

    內部直接調用AQS類的acquire方法,咱們進入該方法

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

    看到沒有與非公平鎖同樣都是調用acquire方法實現鎖獲取,不一樣點只在於tryAcquire方法的實現,這種設計模式也是開發中常常用到的模板方法設計模式,咱們下面貼出FairSync的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;
        }

    方法的大部分邏輯很簡單,咱們看下c = 0條件下調用的hasQueuedPredecessors方法內部做了什麼,該方法是在AQS類定義的。

public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

    hasQueuedPredecessors方法主要判斷當前線程是否位於CLH等待隊列的隊首

    總結ReentrantLock公平鎖的獲取鎖方法lock的基本實現邏輯以下:

1)獲取AQS的同步狀態state和當前線程對象

2)若鎖處於空閒狀態,判斷當前CLH等待隊列中是否存在等待線程,若不存在則獲取鎖成功,直接返回;

3)若鎖處於空閒狀態,CLH等待隊列中存在等待獲取鎖線程則獲取鎖失敗,將當前線程加入到CLH等待隊列末尾,逐步的去執行CLH等待隊列的線程,若是當前線程獲取到了鎖,則返回;不然,當前線程進行暫時休眠,直到喚醒並從新獲取鎖才返回;

4)若鎖已被線程持有且持有線程的是當前線程,則更新同步狀態(鎖持有線程遞歸加鎖次數)state判斷是否超出最大次數限制,若未超出限制則加鎖成功;

    總結ReentrantLock公平鎖和非公平鎖獲取鎖方法的差別性主要表如今獲取鎖的策略方面,多線程環境下線程在獲取非公平鎖的時候只判斷鎖處於空閒狀態就能夠獲取,而公平鎖須要基於CLH等待隊列排隊獲取鎖。

2 - void unlock()方法 - 釋放鎖

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

    方法內部調用了sync.release(int)方法,該方法是在AQS(AbstractQueuedSynchronizer)類實現的,進入該方法

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

    AQS的release方法首先調用了tryRelease方法獲取鎖,該方法的實如今ReentrantLock內部的Sync類中,進入該類的該方法

protected final boolean tryRelease(int releases) {
            // 更新鎖的遞歸加鎖次數state
            int c = getState() - releases;
            // 鎖持有鎖線程不是當前線程拋出異常,只有鎖持有線程才能夠釋放鎖資源
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 若c=0還須要將線程持有者內部成員變量設置爲null
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 不然只是鎖的遞歸釋放,同一線程屢次加鎖須要屢次釋放
            setState(c);
            return free;
        }

    release方法基本流程總結以下:1)判斷鎖持有線程是不是當前線程若不是直接拋出異常;2)若鎖的同步狀態state = 0還須要將鎖持有線程變量設置爲null;3)若只是鎖的遞歸釋放,即當前線程屢次加鎖則須要釋放,除了最後一次釋放須要重置所持有線程爲null以外只須要更新鎖的同步狀態(即state原子遞減)。

    接下來咱們分析下unparkSuccessor方法,該方法的做用是喚醒CLH隊列的後繼等待節點

private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        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);
    }

    下面是線程狀態waitStatus的說明

CANCELLED[1]  -- 當前線程已被取消
SIGNAL[-1]    -- 「當前線程的後繼線程須要被unpark(喚醒)」。通常發生狀況是:當前線程的後繼線程處於阻塞狀態,而當前線程被release或cancel掉,所以須要喚醒當前線程的後繼線程。
CONDITION[-2] -- 當前線程(處在Condition休眠狀態)在等待Condition喚醒
PROPAGATE[-3] -- (共享鎖)其它線程獲取到「共享鎖」
[0]           -- 當前線程不屬於上面的任何一種狀態。

    經過分析源碼可知unparkSuccessor方法的基本邏輯是:

1)首先更新釋放CLH等待隊列中的當前線程;

2)而後喚醒CLH等待隊列當前線程後繼節點中不爲空或者未取消的線程,它首先判斷等待隊列中當前線程節點的後繼節點是不是正常的,如果直接喚醒後繼節點,不然從隊列尾部往前遞歸回溯獲取當前節點後繼節點以後最近一個正常節點, 喚醒它。

    總結ReentrantLock釋放鎖不區分公平鎖和非公平鎖,它的主要流程是1)遞減同步狀態變量state,當state=0的時候釋放鎖將當前鎖持有線程引用置爲null;2)喚醒隊列裏的其餘線程(當前節點第一個正常的後繼節點線程)

相關文章
相關標籤/搜索