【JDK】JDK原始碼分析-ReentrantLock

WriteOnRead發表於2019-08-06

概述

 

在 JDK 1.5 以前,鎖的實現只能用 synchronized 關鍵字;1.5 開始提供了 ReentrantLock,它是 API 層面的鎖。先看下 ReentrantLock 的類簽名以及如何使用:

public class ReentrantLock implements Lock, java.io.Serializable {}

典型用法:

public void m() {
  lock.lock();  // block until condition holds
  try {
    // ... method body
  } finally {
    lock.unlock()
  }
}

該用法和使用 synchronized 關鍵字效果是一樣的。既然有了 synchronized,為什麼又會有 Lock 呢?相比於 synchronized,其實 ReentrantLock 的出現並不重複,它增加了不少功能,下面先簡單介紹幾個概念。

 

公平鎖&非公平鎖:所謂鎖是否公平,簡單理解就是一系列執行緒獲取到鎖的順序是否遵循「先來後到」。即,如果先申請鎖的執行緒先獲取到鎖,就是公平鎖;否則就是非公平鎖。ReentrantLock 的預設實現和 synchronized 都是非公平鎖。

 

可重入鎖:鎖是否可重入,就是一個執行緒是否可以多次獲取同一個鎖,若是,就是可重入鎖。ReentrantLock 和 synchronized 都是可重入鎖。

 

程式碼分析

 

構造器

 

ReentrantLock 有兩個構造器,分別如下:

private final Sync sync;

// 構造一個 ReentrantLock 例項(非公平鎖)
public ReentrantLock() {
    sync = new NonfairSync();
}

// 構造一個 ReentrantLock 例項(指定是否公平)
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

可以看到,兩個構造器都是初始化一個 Sync 型別的成員變數。而且,當 boolean 值 fair 為 true 時,初始化的 sync 為 FairSync,為 false 時初始化為 NonFairSync,二者分別表示「公平鎖」和「非公平鎖」。可以看到無參構造預設是非公平鎖。

 

常用方法

 

ReentrantLock 常用的方法就是 Lock 介面定義的幾個方法,如下:

// 獲取鎖(阻塞式)
public void lock() {
    sync.lock();
}

// 獲取鎖(響應中斷)
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

// 嘗試獲取鎖
public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}

// 嘗試獲取鎖(有超時等待)
public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

// 釋放鎖
public void unlock() {
    sync.release(1);
}

可以看到,這幾個方法內部都是通過呼叫 Sync 類(或其子類)的方法來實現,因此先從 Sync 類入手分析,程式碼如下(部分省略):

// 抽象類,繼承了 AQS
abstract static class Sync extends AbstractQueuedSynchronizer {

    // 獲取鎖的方法,由子類實現
    abstract void lock();

    // 非公平鎖的 tryLock 方法實現
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        // 獲取 AQS 的 state 變數
        int c = getState();
        // 若為 0,表示當前沒有被其他執行緒佔用
        if (c == 0) {
            // CAS 修改 state,若修改成功,表示成功獲取資源
            if (compareAndSetState(0, acquires)) {
                // 將當前執行緒設定為 owner,到這裡表示當前執行緒成功獲取資源
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // state 不為 0,且 owner 為當前執行緒
        // 表示當前執行緒已經獲取到了資源,這裡表示“重入”
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            // 修改 state 值(因為當前執行緒已經獲取資源,不存在競爭,因此無需 CAS 操作)
            setState(nextc);
            return true;
        }
        return false;
    }

    // 釋放鎖操作(對 state 做減法)
    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;
            // 成功釋放後將 owner 設為空
            setExclusiveOwnerThread(null);
        }
        // 修改 state 的值
        // PS: 因為可能存在“重入”,因此一次釋放操作後當前執行緒仍有可能佔用資源,
        // 所以不會直接把 state 設為 0
        setState(c);
        return free;
    }
    
    // 其他方法...
    
    final boolean isLocked() {
        return getState() != 0;
    }
}

Sync 類繼承自 AQS,其中 nonfairTryAcquire 方法是非公平鎖 tryAcquire 方法的實現。

 

從上面程式碼可以看出,鎖的獲取和釋放是通過修改 AQS 的 state 變數來實現的。lock 方法可以看做對 state 執行“加法”操作,而 unlock 可以看做對 state 執行“減法”操作,當 state 為 0 時,表示當前沒有執行緒佔用資源。

 

公平鎖&非公平鎖

 

(1)非公平鎖 NonFairSync:

static final class NonfairSync extends Sync {
    
    final void lock() {
        // CAS 嘗試將 state 值修改為 1
        if (compareAndSetState(0, 1))
            // 若修改成功,則將當前執行緒設為 owner,表示成功獲取鎖
            setExclusiveOwnerThread(Thread.currentThread());
        // 若獲取失敗,則執行 AQS 的 acquire 方法(獨佔模式獲取資源)
        else
            acquire(1);
    }
    
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

可以看到,非公平鎖的 lock 操作為:先嚐試以 CAS 方式修改 state 的值,若修改成功,則表示成功獲取到鎖,將 owner 設為當前執行緒;否則就執行 AQS 中的 acquire 方法,具體可參考前文「JDK原始碼分析-AbstractQueuedSynchronizer(2)」,這裡不再贅述。

 

(2)公平鎖 FairSync:

static final class FairSync extends Sync {

    final void lock() {
        acquire(1);
    }
    
    // 公平鎖的 tryAcquire 實現
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        // state 為 0,表示資源未被佔用
        if (c == 0) {
            // 若佇列中有其他執行緒在排隊等待,則返回 false,表示獲取失敗;
            //   否則,再嘗試去修改 state 的值
            // PS: 這裡是公平鎖與非公平鎖的區別所在
            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;
    }
}

可以看到,與非公平鎖相比,公平鎖的不同之處在於增加了判斷條件 hasQueuedPredecessors,即首先判斷主佇列中是否有其他執行緒在等待,當沒有其他執行緒在排隊時再去獲取,否則獲取失敗。

 

hasQueuedPredecessors 在 AQS 中實現如下:

/**
 * Queries whether any threads have been waiting to acquire longer
 * than the current thread.
 */
public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

 

小結

 

synchronized 與 ReentrantLock 比較:

相同點:二者都是互斥鎖,可重入,預設都是非公平鎖。

不同點:synchronized 是語法層面實現,自動獲取鎖和釋放鎖;ReentrantLock 是 API 層面實現,手動獲取鎖和釋放鎖。

 

ReentrantLock 相比 synchronized 的優勢:

1. 可響應中斷;

2. 獲取鎖可設定超時;

3. 可實現公平鎖;

4. 可繫結多個條件(Condition)。

 

JDK 1.6 以後,synchronized 與 ReentrantLock 效能基本持平,JVM 未來的效能優化也會更偏向於原生的 synchronized。因此,如何選擇還要根據實際需求,效能不再是不選擇 synchronized 的原因了。

 

相關閱讀:

JDK原始碼分析-Lock&Condition

JDK原始碼分析-AbstractQueuedSynchronizer(2)

 

 

Stay hungry, stay foolish.

PS: 本文首發於微信公眾號【WriteOnRead】。

相關文章