Java中的鎖 -- 開篇

strind發表於2024-10-20

Java中的鎖

1. 頂級介面Lock

Java SE5之後併發包中新增了Lock介面,它提供了與synchronized關鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖。雖然它缺少了隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性可中斷的獲取鎖以及超時獲取鎖等多種synchronized 關鍵字所不具備的同步特性。

看一下Lock介面的定義:

public interface Lock {

    // 獲取鎖,呼叫該方法當前執行緒將會獲取鎖,當鎖獲得後,從該方法返回
    void lock();

    // 可中斷地獲取鎖,和lock()方法的不同之處在於該方法會響應中斷,即在鎖的獲取
	// 中可以中斷當前執行緒
    void lockInterruptibly() throws InterruptedException;

    // 嘗試非阻塞的獲取鎖,呼叫該方法後立刻返回,如果能夠獲取則返回true,否則返false
    boolean tryLock();

    // 超時的獲取鎖,當前執行緒在以下3種情況下會返回:
	// 1. 當前執行緒在超時時間內獲得了鎖
	// 2. 當前執行緒在超時時間內被中斷
	// 3. 超時時間結束,返回 false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    // 釋放鎖
    void unlock();

    // 獲取等待通知元件,該元件和當前的鎖繫結,當前執行緒只有獲得了鎖,才能呼叫該元件的 wait()方法,而呼叫後,當前執行緒將釋放鎖
    Condition newCondition();
}

2. AQS 佇列同步器

佇列同步器AbstractQueuedSynchronizer,是用來構建鎖或者其他同步元件的基礎框架,它使用了一個int成員變數表示同步狀態,透過內建的FIFO佇列來完成資源獲取執行緒的排隊工作。

同步器的主要使用方式是繼承,子類透過繼承同步器並實現它的抽象方法來管理同步狀態,在抽象方法的實現過程中免不了要對同步狀態進行更改,這時就需要使用同步器提供的方法來進行操作,因為它們能夠保證狀態的改變是安全的。

同步器是實現鎖(也可以是任意同步元件)的關鍵。鎖是面向使用者的,它定義了使用者與鎖互動的介面(比如Lock和UnLock等),隱藏了實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,遮蔽了同步狀態管理、執行緒的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現者所需關注的領域。

2.1 佇列同步器的介面

同步器的設計是典型的模板方法,也就是說,使用者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步元件的實現中,並呼叫同步器提供的模板方法,而這些模板方法將會呼叫使用者重寫的方法。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    private transient volatile Node head;

    private transient volatile Node tail;

    private volatile int state;

    // 設定和獲取同步狀態
    protected final int getState() {
        return state;
    }

    protected final void setState(int newState) {
        state = newState;
    }

    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

    // 獨佔式獲取同步狀態,實現該方法需要查詢當前狀態並判斷同步狀態是否符合預期,然後再進行CAS設定同步狀態
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

    // 獨佔式釋放同步狀態,等待獲取同步狀態的執行緒將有機會獲取同步狀態
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

    // 共享式獲取同步狀態,返回大於等於0的值,表示獲取成功,反之,獲取失敗
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

    // 共享式釋放同步狀態
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }

    // 當前同步器是否在獨佔模式下被執行緒佔用,一般該方法表示是否被當前執行緒所獨佔
    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

    // 獨佔式獲取同步狀態,如果當前執行緒獲取同步狀態成功,則由該方法返回,否則,將會進入同步佇列等待,該方法將會呼叫重寫的tryAcquire(intarg)方法
    public final void acquire(int arg) {
        // .....
    }
    
    // 與acquire(int arg)相同,但是該方法響應中斷,當前執行緒未獲取到同步狀態而進人同步佇列中,如果當前執行緒被中斷,則該方法會丟擲InterruptedException並返回
    public final void acquireInterruptibly(int arg)
        throws InterruptedException {
        // .....
    }

    // 在 acquireInterruptibly(int arg)基礎上增加了超時限制,如果當前執行緒在超時時間內沒有獲取到同步狀態,那麼將會返回false,如果獲取到了返回true
    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
        // .....
    }

    // 獨佔式的釋放同步狀態,該方法會在釋放同步狀態之後,將同步佇列中第一個節點包含的執行緒喚醒
    public final boolean release(int arg) {
        //.....
    }

    // 共享式的獲取同步狀態,如果當前執行緒未獲取到同步狀態,將會進人同步佇列等待,與獨佔式獲取的主要區別是在同一時刻可以有多個執行緒獲取到同步狀態
    public final void acquireShared(int arg) {
        // .....
    }
    
    // 與acquireShared(int arg)相同,該方法響應中斷
    public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
       // .....
    }

    // 在 acquireSharedlnterruptibly(int arg)基礎上增加了超時限制
    public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        // ......
    }
    
    // 共享式的釋放同步狀態
    public final boolean releaseShared(int arg) {
        // ....
    }
    
   // 獲取等待在同步佇列上的執行緒集合
    public final Collection<Thread> getQueuedThreads() {
       // .....
    }
}

同步器提供的模板方法基本上分為3類:獨佔式獲取與釋放同步狀態、共享式獲取與釋放同步狀態以及查詢同步佇列中的等待執行緒情況。自定義同步元件將使用同步器提供的模板方法來實現自己的同步語義。

2.2 同步佇列的原始碼分析

這裡分析的程式碼包括:同步佇列、獨佔式同步狀態獲取與釋放、共享式同步狀態獲取與釋放以及超時獲取同步狀態等。

2.2.1 同步佇列

同步器依賴內部的同步佇列(一個FIFO雙向佇列,注意與之後介紹的等待佇列不同)來完成同步狀態的管理,當前執行緒獲取同步狀態失敗時,同步器會將當前執行緒以及等待狀態等資訊構造成為一個節點(Node)並將其加入同步佇列,同時會阻塞當前執行緒,當同步狀態釋放時,會把首節點中的執行緒喚醒使其再次嘗試獲取同步狀態。

同步佇列中的節點(Node)用來儲存獲取同步狀態失敗的執行緒引用、等待狀態以及前驅和後繼節點。

static final class Node {
        volatile Node prev;  // 前驅節點
        volatile Node next;  // 後繼節點
        volatile Thread thread;  // 獲取同步狀態的執行緒
        Node nextWaiter; // 等待佇列中的後繼節點。如果當前節點是共享的,那麼這個欄位將是一個SHARED常量,也就是說節點型別(獨佔和共享)和等待佇列中的後繼節點共用同一個欄位
        volatile int waitStatus;  // 等待狀態

    // 包含如下狀態。
    //1. CANCELLED,值為1,由於在同步佇列中等待的執行緒等待超時或者被中斷,需要從同步佇列中取消等待,節點進人該狀態將不會變化
	//2. SIGNAL,值為-1,後繼節點的執行緒處於等待狀態,而當前節點的執行緒如果釋放了同步狀態或者被取消,將會通知後繼節點,使後繼節點的執行緒得以執行
	//3. CONDITION,值為-2,節點在等待佇列中,節點執行緒等待在Condition上,當其他執行緒對Condition呼叫了signal()方法後,該節點將會從等待佇列中轉移到同步佇列中,加人到對同步狀態的獲取中
	//4. PROPAGATE,值為-3,表示下一次共享式同步狀態獲取將會無條件地被傳播下去
	//5.  INITIAL,值為 0,初始狀態
}

節點是構成 同步佇列/等待佇列 的基礎,同步器擁有首節點(head)和尾節點(tail),沒有成功獲取同步狀態的執行緒將會成為節點加入該佇列的尾部,形成連結串列的結構。

同步佇列遵循FIFO,首節點是獲取同步狀態成功的節點(但此時正在執行該執行緒的邏輯,還沒有釋放資源,資源可以簡單理解為鎖),首節點的執行緒在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在獲取同步狀態成功時將自己設定為首節點。設定首節點是透過獲取同步狀態成功的執行緒來完成的,因為只有一個執行緒能夠成功獲取到同步狀態,因此設定頭節點的方法本身就是執行緒安全的。

2.2.2 獨佔式同步狀態的獲取和釋放

透過呼叫同步器的acquire(int arg)方法可以獲取同步狀態,該方法對中斷不敏感,也就是執行緒由於獲取同步狀態失敗後進入同步佇列中,後續對執行緒進行中斷操作時,執行緒不會從同步佇列中移出。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
程式碼功能:完成了同步狀態獲取、節點構造、加人同步佇列以及在同步佇列中自旋等待的相關工作,其主要邏輯是:首先呼叫自定義同步器實現的tyAcquire(int arg)方法,該方法保證執行緒安全的獲取同步狀態,如果同步狀態獲取失敗,則構造同步節點(獨佔式Node.EXCLUSIVE,同一時刻只能有一個執行緒成功獲取同步狀態)並透過addWaiter(Node node)方法將該節點加人到同步佇列的尾部,最後呼叫acquireQueued(Node node,int arg)方法,使該節點以“死迴圈”的方式獲取同步狀態。如果獲取不到則阻塞節點中的執行緒,而被阻塞執行緒的喚醒主要依靠前驅節點的出隊或阻塞執行緒被中斷來實現。
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 首先嚐試在尾節點插入,如果成功,則不需要進入死迴圈
    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) { 
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

節點進入同步佇列之後,就進人了一個自旋的過程,每個節點(或者說每個執行緒)都在自省地觀察,當條件滿足,獲取到了同步狀態,就可以從這個自旋過程中退出,否則依舊在這個自旋過程中(並會阻塞節點的執行緒)。

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);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

當前執行緒獲取同步狀態並執行了相應邏輯之後,就需要釋放同步狀態,使得後續節點能夠繼續獲取同步狀態。透過呼叫同步器的release(int arg)方法可以釋放同步狀態,該方法在釋放了同步狀態之後,會喚醒其後繼節點(進而使後繼節點重新嘗試獲取同步狀態)。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 喚醒後繼節點
            unparkSuccessor(h);
        return true;
    }
    return false;
}

總結:在獲取同步狀態時,同步器維護一個同步佇列,獲取狀態失敗的執行緒都會被加入到佇列中並在佇列中進行自旋;移出佇列(或停止自旋)的條件是前驅節點為頭節點且成功獲取了同步狀態。在釋放同步狀態時同步器呼叫release(int arg)方法釋放同步狀態,然後喚醒頭節點的後繼節點。

2.2.3共享式的同步狀態獲取與釋放

共享式獲取與獨佔式獲取最主要的區別在於同一時刻能否有多個執行緒同時獲取到同步狀態。

透過呼叫同步器的acquireShared(int arg)方法可以共享式地獲取同步狀態。

public final void acquireShared(int arg) {
    // 呼叫tryAcquireShared(int arg)方法嘗試獲取同步狀態,tryAcquireShared(int arg)方法返回值為int型別,當返回值大於等於0時,表示能夠獲取到同步狀態。
    if (tryAcquireShared(arg) < 0)
        // 無法獲取同步狀態,
        doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {  // 前驅節點是頭節點
                int r = tryAcquireShared(arg);
                if (r >= 0) { // 獲得到同步狀態
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

與獨佔式一樣,共享式獲取也需要釋放同步狀態,透過呼叫releaseShared(int arg)方法可以釋放同步狀態。

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            
                unparkSuccessor(h); // 喚醒後繼節點
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

2.2.4 獨佔式超時獲取同步狀態

透過呼叫同步器的 doAcquireNanos(int arg,long nanosTimeout)方法可以超時獲取同步態,即在指定的時間段內獲取同步狀態,如果獲取到同步狀態則返回true,否則,返回false。同步器提供了 acquireInterruptibly(int arg)方法,這個方法在等待獲取同步狀態時,如果當前執行緒被中斷,會立刻返回,並丟擲InterruptedException。

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    // 先判斷一下是否有中斷
    if (Thread.interrupted())
        throw new InterruptedException();
    // 先嚐試獲取,失敗則進入doAcquireNanos方法中
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    // 絕對時間上的超時時間
    final long deadline = System.nanoTime() + nanosTimeout;
    // 封裝節點,並加入同步佇列
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
            // 計算還需等待的時間
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                // 阻塞等待
                LockSupport.parkNanos(this, nanosTimeout);
            // 響應中斷
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

3. 操作實踐

要求:設計一個同步工具,該工具在同一時刻,只允許至多三個執行緒同時訪問,超過三個執行緒
的訪問將被阻塞,我們將這個同步工具命名為ThirdLock。

詳見下一章。

相關文章