Java JUC ReentrantLock解析

神祕傑克發表於2022-01-27

獨佔鎖 ReentrantLock 原理

介紹

ReentrantLock 是可重入的獨佔鎖,同時只能有一個執行緒可以獲取該鎖,其他獲取該鎖的執行緒會被阻塞然後放入到該鎖的AQS阻塞佇列中。

它具有與synchronized相同的基本行為和語義,但 ReentrantLock 更靈活、更強大,增加了輪詢、超時、中斷等高階功能,並且還支援公平鎖和非公平鎖

類圖

從類圖可以看到,ReentrantLock 還是使用 AQS 來實現的,並且可以根據引數來選擇其內部是一個公平鎖還是非公平鎖,預設為非公平鎖

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

Sync 類直接繼承 AQS 類,它的子類 NonfairSync 和 FairSync 分別實現了獲取鎖的非公平和公平策略。

static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}

在 ReentrantLock 中 AQS 的 state 狀態值表示執行緒獲取鎖的可重入次數,在預設情況下,state 值為 0 表示當前所沒有任何執行緒持有。當一個執行緒第一次獲取該鎖後,會嘗試使用 CAS 將 state 設定為 1,如果 CAS 成功則當前執行緒獲取該鎖,然後記錄該鎖的持有者為當前執行緒。在該執行緒第二次獲取該鎖後,則會將 state 設定為 2,這就是可重入次數。在該執行緒釋放鎖時,會嘗試使用 CAS 將 state 減 1,如果減 1 後為 0 則釋放鎖。

獲取鎖

void lock()

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

當執行緒呼叫該方法時,如果當前鎖沒有被其他執行緒佔用並且當前執行緒之前沒獲取過該鎖,則當前執行緒獲取,然後設定鎖的擁有者為自己,隨後設定 AQS 的 state 為 1,然後返回。

如果當前執行緒已經獲取過該鎖,則只是簡單的將 AQS 的 state 加 1,然後返回。

如果該鎖已經被其他執行緒所持有,則當前執行緒會進入 AQS 的阻塞佇列中阻塞掛起。

在 ReentrantLock 中的 lock()方法委託給了 sync 類,sync 則根據 ReentrantLock 的建構函式選擇使用 NonfairSync 或 FairSync。

我們先看看非公平鎖的實現。

final void lock() {
    // CAS設定狀態值
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 呼叫AQS的acquire方法
        acquire(1);
}

在 lock()中首先呼叫了 compareAndSetState 方法,因為預設 state 狀態值為 0,所以第一個執行緒在首次呼叫該方法時通過 CAS 會設定為 1,隨後成功獲取到該鎖,然後通過 setExclusiveOwnerThread 方法將鎖持有者設定為當前執行緒。

當有其它執行緒通過 lock()來獲取該鎖時候,會 CAS 失敗進入 else 呼叫 acquire(1)方法並且傳引數為 1,下面我們再看一下 acquire 方法。

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

之前文章講過,AQS 沒有提供 tryAcquire 方法的實現,我們看一下 ReentrantLock 重寫的 tryAcquire 方法,這裡我們還是看非公平鎖的實現。

static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
                //呼叫該方法
        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;
}

該方法首先檢視當前狀態值是否為 0,為 0 則說明鎖目前是空閒狀態,然後嘗試 CAS 獲取鎖,成功後 state 設定為 1,然後鎖持有者為當前執行緒。

如果 state 不為 0,則說明鎖已經被持有,如果持有者正好是當前執行緒則進行 state 加 1,然後返回 true,需要注意,如果 nextc < 0 則說明鎖可能溢位。

如果當前執行緒不是持有者則返回 false 隨後加入 AQS 阻塞佇列。

下面我們看一下公平鎖的實現。

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

公平鎖和非公平鎖的 tryAcquire 方法不同之處就是多了一個 hasQueuedPredecessors()方法,該方法就是實現公平鎖的核心程式碼。

public final boolean hasQueuedPredecessors() {
    //讀取頭節點
    Node t = tail;
    //讀取尾節點
    Node h = head;
    //s是首節點h的後繼節點
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

在該方法中,因為佇列是 FIFO 的,所以需要判斷佇列中有沒有相關執行緒的節點已經在排隊了。有則返回 true 表示執行緒需要排隊,沒有則返回 false 則表示執行緒無需排隊。

首先我們看第一個條件h != t

  • 頭節點和尾節點都為 null,表示佇列都還是空的,甚至都沒完成初始化,那麼自然返回 fasle,無需排隊。
  • 頭節點和尾節點不為 null 但是相等,說明頭節點和尾節點都指向一個元素,表示佇列中只有一個節點,這時候也無需排隊,因為佇列中的第一個節點是不參與排隊的,它持有著同步狀態,那麼第二個進來的節點就無需排隊,因為它的前繼節點就是頭節點,所以第二個進來的節點就是第一個能正常獲取同步狀態的節點,第三個節點才需要排隊,等待第二個節點釋放同步狀態。

接下來我們看第二個條件,(s = h.next) == null,如果h != t && s == null則說明有一個元素將要作為 AQS 的第一個節點入隊,則返回 true。

接下來看第三個條件,s.thread != Thread.currentThread() ,判斷後繼節點是否為當前執行緒。

??‍♀️ 舉例,情況一:

h != t 返回true,(s = h.next) == null 返回false , s.thread != Thread.currentThread()返回false

首先 h != t 返回true,說明佇列中至少有兩個不同節點存在;

(s = h.next) == null 返回false,說明頭節點之後是有後繼節點存在;

s.thread != Thread.currentThread()返回 false,說明當前執行緒和後繼節點相同;

說明已經輪到當前節點去嘗試獲取同步狀態,無需排隊,返回 false

??‍♀️ 舉例,情況二:

h != t 返回true,(s = h.next) == null 返回true

首先 h != t 返回true,說明佇列中至少有兩個不同節點存在;

(s = h.next) == null 返回true,說明頭節點也就是哨兵節點之後沒有後繼節點;

返回 true,說明需要排隊

??‍♀️ 舉例,情況三:

h != t 返回true,(s = h.next) == null 返回false,s.thread != Thread.currentThread()返回true

首先 h != t 返回true,說明佇列中至少有兩個不同節點存在;

(s = h.next) == null 返回false,說明頭節點之後是有後繼節點存在;

s.thread != Thread.currentThread()返回true,說明後繼節點的執行緒不是當前執行緒,說明前面已經有人在排隊了,還是得老老實實排隊。

返回 true,說明需要排隊

void lockInterruptibly()

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        // 如果當前執行緒被中斷,則丟擲異常
        if (Thread.interrupted())
            throw new InterruptedException();
        //嘗試獲取資源
        if (!tryAcquire(arg))
            //呼叫AQS可被中斷的方法
            doAcquireInterruptibly(arg);
}

該方法和 lock()方法類似,只不過它會對中斷進行響應,就是當前執行緒在呼叫該方法時,如果他其他執行緒呼叫了當前執行緒的 interrupt()方法,則當前執行緒會丟擲 InterruptedException 異常。

boolean tryLock()

public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}
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;
}

該方法嘗試獲取鎖,如果當前鎖沒有被其他執行緒持有,則當前執行緒獲取該鎖並返回 true,否則返回 false。

? 注意:該方法不會引起當前執行緒阻塞。

boolean tryLock(long timeout,TimeUnit unit)

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

與 tryLock 不同之處在於,設定了超時時間,如果超時時間到還沒有獲取到該鎖,則返回 false。

釋放鎖

void unlock()

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;
}
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    //如果不是鎖持有者呼叫unlock則丟擲異常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果當前可重入次數為0 則情況鎖持有執行緒
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

該方法首先嚐試釋放鎖,如果 tryRelease()方法返回 true 則釋放該鎖,否則只是減 1,如果不是鎖持有者去呼叫 unlock 則丟擲 IllegalMonitorStateException 異常。

程式碼實踐

/**
 * @author 神祕傑克
 * 公眾號: Java菜鳥程式設計師
 * @date 2022/1/21
 * @Description 使用ReentrantLock實現簡單的執行緒安全List
 */
public class ReentrantLockList {

    private List<String> array = new ArrayList<>();

    private volatile ReentrantLock lock = new ReentrantLock();

    public void add(String e) {
        lock.lock();
        try {
            array.add(e);
        } finally {
            lock.unlock();
        }
    }

    public void remove(String e) {
        lock.lock();
        try {
            array.remove(e);
        } finally {
            lock.unlock();
        }
    }

    public String get(int index) {
        lock.lock();
        try {
            return array.get(index);
        } finally {
            lock.unlock();
        }
    }

}

該類在通過操作 array 之前,通過加鎖來保證同一時間只有一個執行緒可以操作 array,但是也只能有一個執行緒可以對 array 元素進行訪問。

總結

當同時有三個執行緒嘗試獲取獨佔鎖 ReentrantLock 時,如果執行緒 1 獲取到,則執行緒 2、3 都會被轉換為 Node 節點隨後被放入 ReentrantLock 對應的 AQS 阻塞佇列中然後被掛起。

阻塞佇列執行緒2,執行緒3

假設執行緒 1 在獲取到鎖之後,呼叫了鎖建立的條件變數 1 進入 await 後,執行緒 1 就會釋放該鎖。然後執行緒 1 會被轉換為 Node 節點插入條件變數 1 的條件佇列中。

由於執行緒 1 釋放了鎖,所以阻塞佇列中的執行緒 2,執行緒 3 都會有機會獲取到該鎖,假如使用的是公平鎖,那麼執行緒 2 則會獲取到該鎖,然後從 AQS 阻塞佇列中移除執行緒 2 對應的 Node 節點。

阻塞佇列執行緒3

相關文章