java中的鎖及實現原理

strind發表於2024-10-22

重入鎖ReentrantLock

重人鎖ReentrantLock,顧名思義,就是支援重進人的鎖,它表示該鎖能夠支援一個執行緒對資源的重複加鎖。除此之外,該鎖的還支援獲取鎖時的公平和非公平性選擇。

ReentrantLock雖然沒能像synchronized關鍵字一樣支援隱式的重進人,但是在調lock( )方法時,已經獲取到鎖的執行緒,能夠再次呼叫1ock()方法獲取鎖而不被阻塞。

下面將重點分析ReentrantLock是如何實現重進入和公平性獲取鎖的特性。

1. 實現重進入

重進入是指任意執行緒在獲取到鎖之後能夠再次獲取該鎖而不會被鎖所阻塞,該特性的現需要解決以下兩個問題。

  1. 執行緒再次獲取鎖。鎖需要去識別獲取鎖的執行緒是否為當前佔據鎖的執行緒、如果是則再次成功獲取。
  2. 鎖的最終釋放。執行緒重複n次獲取了鎖,隨後在第n次釋放該鎖後,其他執行緒才能獲取到該鎖。鎖的最終釋放要求鎖對於獲取進行計數自增,計數表示當前鎖被重複獲取的數,而鎖被釋放時,計數自減,當計數等於0時表示鎖已經成功釋放。
static final class NonfairSync extends Sync {
    // 獲取鎖的邏輯
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
abstract static class Sync extends AbstractQueuedSynchronizer {
    
    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) 
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

成功獲取鎖的執行緒再次獲取鎖,只是增加了同步狀態值,這也就要求ReentrantLock在釋放同步狀態時減少同步狀態值。

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;
        // 不在有執行緒持有鎖
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

2. 公平鎖與非公平鎖的區別

公平性與否是針對獲取鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求的絕對時間順序,也就是FIFO。

// 公平鎖
static final class FairSync extends Sync {
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // hasQueuedPredecessors()會判斷是否有其他執行緒在等待著,如果有則返回true
            // 當前執行緒不在透過compareAndSetState搶佔鎖,直接進入同步佇列或重入鎖的邏輯
            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;
    }
}

非公平性鎖可能使執行緒“飢餓”,但是通常會帶來更好的效能。

讀寫鎖

讀寫鎖允許在同一時刻可以允許多個讀執行緒訪問,但是在寫執行緒訪問時,所有的讀執行緒和其他寫執行緒均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,透過分離讀鎖和寫鎖,使得併發性相比一般的排他鎖有了很大提升。

一般情況下,讀寫鎖的效能都會比排它鎖好,因為大多數場景讀是多於寫的。在讀多於後的情況下,讀寫鎖能夠提供比排它鎖更好的併發性和吞吐量。java併發包提供讀寫鎖的示例是 RcentrantReadWriteLock,有如下幾個特性:

  1. 支援非公平(預設)和公平的鎖獲取方式,吞吐量還是非公平優於公平
  2. 該鎖支援重進人,以讀寫執行緒為例:讀執行緒在獲取了讀鎖之後,能夠再次獲取鎖。而寫執行緒
    在獲取了寫鎖之後能夠再次獲取寫鎖,同時也可以獲取讀鎖
  3. 遵循獲取寫鎖、獲取讀鎮再釋放寫鎖的次序,寫鎖能夠降級成為讀鎖

1. 讀寫鎖的介面

public interface ReadWriteLock {
   // 獲取讀鎖
    Lock readLock();

    // 獲取寫鎖
    Lock writeLock();
}

2. 程式碼分析

接下來分析 ReentrantReadWriteLock 的實現,主要包括:讀寫狀態的設計、寫鎖的獲取與釋放、讀鎖的獲取與釋放以及鎖降級

2.1 讀寫狀態的設計

讀寫鎖同樣依賴自定義同步器來實現同步功能,而讀寫狀態就是其同步器的同步狀態。讀寫鎖的自定義同步器需要在同步狀態(一個整型變數)上維護多個讀執行緒和一個寫執行緒的狀態,使得該狀態的設計成為讀寫鎖實現的關鍵。

如果在一個整型變數上維護多種狀態,就一定需要“按位切割使用”這個變數,讀寫鎖將變數切分成了兩個部分,高16位表示讀,低 16位表示寫。

2.2 寫鎖的釋放與獲取

寫鎖是一個支援重進人的排它鎖。如果當前執行緒已經獲取了寫鎖,則增加寫狀態。如果當前執行緒在獲取寫鎖時,讀鎖已經被獲取(讀狀態不為0)或者該執行緒不是已經獲取寫鎖的執行緒,則當前執行緒進入等待狀態。

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c); // 寫鎖的個數
    if (c != 0) {
        // 存在讀鎖 或 執行緒不是已經獲取寫鎖的執行緒
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // 重入
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

2.3 讀鎖的獲取與釋放

讀鎖是一個支援重進入的共享鎖,它能夠被多個執行緒同時獲取,在沒有其他寫執行緒訪問(或者寫狀態為0)時,讀鎖總會被成功地獲取,而所做的也只是(執行緒安全的)增加狀態。如果當前執行緒已經獲取了讀鎖,則增加讀狀態。如果當前執行緒在獲取讀鎖時,寫鎖被其他執行緒獲取,則進人等待狀態。

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        } else if (readerShouldBlock()) {
            // Make sure we're not acquiring read lock reentrantly
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

2.4 鎖降級

鎖降級指的是寫鎖降級成為讀鎖。如果當前執行緒擁有寫鎖,然後將其釋放,最後再獲取讀鎖,這種分段完成的過程不能稱之為鎖降級。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。

Condition介面

任意一個Java物件,都擁有一組監視器方法(定義在java.lang.0bject上),主要包括wait( )、wait(long timeout)、notify( ) 以及notifyA1I( )方法,這些方法與synchronized 同步關鍵字配合,可以實現等待/通知模式。Condition介面也提供了類似Object的監視器方法,與lock配合可以實現等待/通知模式。

1. 介面與示例

Condition定義了等待/通知兩種型別的方法,當前執行緒呼叫這些方法時、需要提前獲取到Condition對架關聯的鎖,Condition物件是由 Lock 物件建立出來的,換句話說,Condition是依賴Lock物件的。

Condition的使用方式比較簡單,需要注意在呼叫方法前獲取鎖,使用方式如代。

/**
 * @author strind
 * @date 2024/10/20 11:55
 * @description Condition使用方式
 */
public class ConditionUserDemo {
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    public void ConditionWait() throws InterruptedException {
        lock.lock();
        try {
            condition.await();
        }finally {
            lock.unlock();
        }
    }

    public void ConditionWake() throws InterruptedException {
        lock.lock();
        try {
            condition.signal();
        }finally {
            lock.unlock();
        }
    }

}

如示例所示,一般都會將Condition物件作為成員變數。當呼叫await( )方法後,當前執行緒會釋放鎖並在此等待,而其他執行緒呼叫Condition物件的signal( )方法,通知當前執行緒後,當前執行緒才從 await( )方法返回,並且在返回前已經獲取了鎖。

public interface Condition {
    // 當前執行緒進入等待狀態直到被通知(signal)或中斷,當前執行緒將進人執行狀態並從await()方法返回的情況,包括:
    // 1. 其他執行緒呼叫該Conditon的signal()或signalAll()方法,而當前執行緒被選中喚醒
    // 2. 其他執行緒(呼叫interrupt()方法)中斷當前執行緒
    // 如果當前等待執行緒從awai()方法返回,那麼表明該執行緒已經獲取了Condition物件所對應的鎖
    void await() throws InterruptedException;
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    boolean awaitUntil(Date deadline) throws InterruptedException;
    // 喚醒一個等待在Condition上的執行緒,該執行緒從等待方法返回前必須獲得與Condition 相關聯的鎖
    void signal();
    // 喚醒所有
    void signalAll();
}

2. 原理分析

ConditionObject是同步器AbstractQueuedSynchronizer的內部類,因為Condition 的操作需要獲取相關聯的鎖,所以作為同步器的內部類也較為合理。每個Condition物件都包含著一個佇列(以下稱為等待佇列),該佇列是Condition物件實現等待/通知功能的關鍵。

Condition的實現,主要包括:等待佇列、等待和通知,下面提到的Condition 如果不加說明均指的是 ConditionObject。

2.1等待佇列

等待佇列是一個FIFO的佇列,在佇列中的每個節點都包含了一個執行緒引用,該執行緒就是在 Condition物件上等待的執行緒,如果一個執行緒呼叫了Condition.await( )方法,那麼該執行緒將會釋放鎖、構造成節點加入等待佇列並進人等待狀態。事實上,節點的定義複用了同步器中節點的定義,也就是說,同步佇列和等待佇列中節點型別都是同步器的靜態內部類AbstractQueuedSynchronizer.Node

一個Condition包含一個等待佇列,Condition擁有首節點和尾節點。當前執行緒呼叫 Condition.await( )方法,將會以當前執行緒構造節點,並將節點從尾部加人等待佇列。

2.2 等待

呼叫 Condition 的await( )方法(或者以await開頭的方法),會使當前執行緒進人等待佇列並釋放鎖,同時執行緒的狀態變更為等待狀態。當從await( )方法返回時,當前執行緒一定是獲取了Condition相關的鎖。

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 加入等待佇列
    Node node = addConditionWaiter();
    // 釋放同步狀態,即釋放鎖
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

呼叫該方法的執行緒是成功獲取了鎖的執行緒,也就是同步佇列中的首節點,該方法會將當前執行緒構造成節點並加入等待佇列中,然後釋放同步狀態,喚醒同步佇列中的後繼節點,然後當前執行緒會進入等待狀態。

當等待佇列中的節點被喚醒,則喚醒節點的執行緒開始嘗試獲取同步狀態。如果不是透過其他執行緒呼叫Condition.signal( )方法喚醒,而是對等待執行緒進行中斷,則會丟擲InterruptedException.

2.3 通知

呼叫 Condition 的signal()方法,將會喚醒在等待佇列中等待時間最長的節點(首節點),在喚醒節點之前,會將節點移到同步佇列中。

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

呼叫該方法的前置條件是當前執行緒必須獲取了鎖,可以看到signal( )方法進行了isHeldExclusively( )檢查,也就是當前執行緒必須是獲取了鎖的執行緒。接著獲取等待佇列的首節點,將其移動到同步佇列並使用LockSuppont 喚醒節點中的執行緒。

透過呼叫同步器的 enq (Node node)方法,等待佇列中的頭節點執行緒安全地移動到同步佇列,當節點移動到同步佇列後,當前執行緒再使用LockSupport 喚醒該節點的執行緒。

被喚醒後的執行緒,將從await( )方法中的 while迴圈中退出(isOnSyncQueue(Node node)方法返回tnue,節點已經在同步佇列中),進而呼叫同步器的acquireQueued( )方法加人到或取同步狀態的競爭中。

成功獲取同步狀態(或者說鎖)之後,被喚醒的執行緒將從先前呼叫的await0方法返回,此時該執行緒已經成功地獲取了鎖。

Condition的signalAIl( ) 方法,相當於對等待佇列中的每個節點均執行一次 signal( )方法,效果就是將等待佇列中所有節點全部移動到同步佇列中,並喚醒每個節點的執行緒。

相關文章