1. 程式人生 > 實用技巧 >ReentrantLock與synchronized 原始碼解析

ReentrantLock與synchronized 原始碼解析

一.概念及執行原理

  在 JDK 1.5 之前共享物件的協調機制只有 synchronized 和 volatile,在 JDK 1.5 中增加了新的機制 ReentrantLock,該機制的誕生並不是為了替代 synchronized,而是在 synchronized 不適用的情況下,提供一種可以選擇的高階功能。

二.synchronized 和 ReentrantLock 的實現和區別

1.實現的方式

synchronized

  • synchronized 屬於獨佔式悲觀鎖,是通過 JVM 隱式實現的
  • synchronized 只允許同一時刻只有一個執行緒操作資源。

在 Java 中每個物件都隱式包含一個 monitor(監視器)物件,加鎖的過程其實就是競爭 monitor 的過程,當執行緒進入位元組碼 monitorenter 指令之後,執行緒將持有 monitor 物件,執行 monitorexit 時釋放 monitor 物件,當其他執行緒沒有拿到 monitor 物件時,則需要阻塞等待獲取該物件。

ReentrantLock

  • ReentrantLock 是 Lock 的預設實現方式之一,它是基於 AQS(Abstract Queued Synchronizer,佇列同步器)實現的
  • 它預設是通過非公平鎖實現的,在它的內部有一個 state 的狀態欄位用於表示鎖是否被佔用,如果是 0 則表示鎖未被佔用,此時執行緒就可以把 state 改為 1,併成功獲得鎖,而其他未獲得鎖的執行緒只能去排隊等待獲取鎖資源。

2.區別

(1)效能

synchronized 和 ReentrantLock 都提供了鎖的功能,具備互斥性和不可見性。在 JDK 1.5 中 synchronized 的效能遠遠低於 ReentrantLock,但在 JDK 1.6 之後 synchronized 的效能略低於 ReentrantLock

(2)實現方式

  • synchronized 是 JVM 隱式實現的、
  • ReentrantLock 是 Java 語言提供的 API;
    ReentrantLock 可設定為公平鎖,而 synchronized 卻不行;

(3)作用域

  • ReentrantLock 只能修飾程式碼塊
  • synchronized 可以用於修飾方法、修飾程式碼塊等;

(4)鎖的釋放

  • ReentrantLock 需要手動加鎖和釋放鎖,如果忘記釋放鎖,則會造成資源被永久佔用
  • synchronized 無需手動釋放鎖;
    ReentrantLock 可以知道是否成功獲得了鎖,而 synchronized 卻不行。

三.ReentrantLock 原始碼分析(ReentrantLock 的具體實現細節)

1.ReentrantLock 的兩個建構函式

首先來看 ReentrantLock 的兩個建構函式:

public ReentrantLock() {
    sync = new NonfairSync(); // 非公平鎖
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

無參的建構函式建立了一個非公平鎖,使用者也可以根據第二個建構函式,設定一個 boolean 型別的值,來決定是否使用公平鎖來實現執行緒的排程。

2.公平鎖 VS 非公平鎖

公平鎖

公平鎖執行緒需要按照請求的順序來獲得鎖

非公平鎖

非公平鎖允許“插隊”的情況存在,所謂的“插隊”指的是,執行緒在傳送請求的同時該鎖的狀態恰好變成了可用,那麼此執行緒就可以跳過佇列中所有排隊的執行緒直接擁有該鎖。

3.ReentrantLock 非公平鎖原始碼

公平鎖由於有掛起和恢復所以存在一定的開銷,因此效能不如非公平鎖,所以 ReentrantLock 和 synchronized 預設都是非公平鎖的實現方式。

3.1 ReentrantLock的加鎖和解鎖方法

ReentrantLock 是通過 lock() 來獲取鎖,並通過 unlock() 釋放鎖,使用程式碼如下:

Lock lock = new ReentrantLock();
try {
    // 加鎖
    lock.lock();
    //......業務處理
} finally {
    // 釋放鎖
    lock.unlock();
}

3.2 加鎖的流程

ReentrantLock 中的 lock() 是通過 sync.lock()實現 的,但 Sync 類中的 lock() 是一個抽象方法,需要子類 NonfairSyncFairSync 去實現,

3.2.1 NonfairSync 中的 lock() 原始碼
final void lock() {
    if (compareAndSetState(0, 1))
        // 將當前執行緒設定為此鎖的持有者
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
3.2.2 FairSync 中的 lock() 原始碼
final void lock() {
    acquire(1);
}

可以看出非公平鎖比公平鎖只是多了一行 compareAndSetState 方法,該方法是嘗試將 state 值由 0 置換為 1,如果設定成功的話,則說明當前沒有其他執行緒持有該鎖,不用再去排隊了,可直接佔用該鎖,否則,則需要通過 acquire 方法去排隊。

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

tryAcquire 方法嘗試獲取鎖,如果獲取鎖失敗,則把它加入到阻塞佇列中,來看 tryAcquire 的原始碼:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 公平鎖比非公平鎖多了一行程式碼 !hasQueuedPredecessors() 
        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); // set state=state+1
        return true;
    }
    return false;
}

對於此方法來說,公平鎖比非公平鎖只多一行程式碼 !hasQueuedPredecessors(),它用來檢視佇列中是否有比它等待時間更久的執行緒,如果沒有,就嘗試一下是否能獲取到鎖,如果獲取成功,則標記為已經被佔用。

如果獲取鎖失敗,則呼叫 addWaiter 方法把執行緒包裝成 Node 物件,同時放入到佇列中,但 addWaiter 方法並不會嘗試獲取鎖,acquireQueued 方法才會嘗試獲取鎖,如果獲取失敗,則此節點會被掛起,

3.2.5 acquireQueued 原始碼
/**
 * 佇列中的執行緒嘗試獲取鎖,失敗則會被掛起
 */
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); // 獲取成功,將當前節點設定為 head 節點
                p.next = null; // 原 head 節點出隊,等待被 GC
                failed = false; // 獲取成功
                return interrupted;
            }
            // 判斷獲取鎖失敗後是否可以掛起
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 執行緒若被中斷,返回 true
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

該方法會使用 for(;