Java併發--隊列同步器AbstractQueuedSynchronizer

前言

在Java的多線程環境,如果需要控制多個線程訪問共享資源時,我們可以使用synchronized來提供同步,實現鎖功能。在Java SE 5之後,Java併發包新增了Lock接口以及相關實現類,我們可以使用Lock實現與synchronized相同的鎖功能。由於Lock鎖在使用時需要顯式地區獲取和釋放鎖,使鎖的釋放和獲取具有可操作性,也衍生了一些功能各異的鎖。例如可重入鎖ReentrantLock,可重入讀寫鎖ReentrantReadWriteLock。當我們去分析ReentrantLock,ReentrantReadWriteLock的源碼時,不難發現這些鎖功能的實現均由隊列同步器AbstractQueuedSynchronizer提供。

正文

1.什麼是同步器AbstractQueuedSynchronizer

隊列同步器AbstractQueuedSynchronizer是面向鎖開發者的一個基礎框架,我們可以利用隊列同步器AbstractQueuedSynchronizer構建鎖或者其他同步組件。在AbstractQueuedSynchronizer中使用一個int成員變量state表示同步狀態,同時使用一個先進先出的隊列來完成資源獲取線程的排隊工作(在AbstractQueuedLongSynchronizer中使用long成員變量state表示同步狀態)。

同步器是實現鎖以及同步組件的關鍵,在鎖的內部中使用同步器去實現鎖的語義。鎖是面對使用者,它定義了鎖使用者和鎖交互的接口。而同步器是面向鎖的實現者,簡化了鎖的實現方式。

2.同步器AbstractQueuedSynchronizer內部類Node

Node代表同步隊列的同步節點。同步隊列用於管理獲取同步狀態失敗的線程。當前線程獲取同步狀態失敗時,同步器將當前線程構造成一個Node節點,將其加入到同步隊列尾部,同時阻塞當前線程,而當同步狀態被釋放時,會喚醒同步隊列的頭節點,頭節點嘗試獲取同步狀態。

Node類有一個重要的成員變量waitStatus,代表Node節點的等待狀態。

  • CANCELLED:值爲1,表示取消等待狀態
  • INITIAL:值爲0,表示初始狀態
  • SIGNAL:值爲-1,表示後繼節點的線程處於等待狀態
  • CONDITION:值爲-2,表明節點線程在Condition等待
  • PROPAGATE:值爲-3,表明下一次共享同步狀態獲取會無條件傳播下去

注:waitStatus爲負值表示節點處於等待狀態,waitStatus爲0表示初始狀態,waitStatus爲正值表示處於取消狀態

3.實現分析

在同步器AbstractQueuedSynchronizer中有幾個重要的成員變量。

// 同步隊列頭節點
private transient volatile Node head;
// 同步隊列尾節點
private transient volatile Node tail;
// 同步狀態
private volatile int state;

在同步器AbstractQueuedSynchronizer分析之前,首先解釋一下什麼是獨佔式獲取鎖,什麼是共享式獲取鎖。以讀寫鎖爲例,一個資源可以同時被多個線程讀,但是隻能被一個線程寫。那麼這個讀所使用的鎖就是共享式鎖,允許多個線程獲取鎖,這個寫就是獨佔式鎖,只允許一個線程獲取鎖。在ReentrantReadWriteLock源碼中我們也可以發現有兩個鎖,一個是讀鎖ReadLock,而一個是寫鎖WriteLock。讀鎖ReadLock與寫鎖WriteLock聯合使用實現讀寫鎖的功能。

無論是獨佔式獲取鎖,還是共享式獲取鎖,在同步器AbstractQueuedSynchronizer的實現中都是體現在同步狀態的獲取和更改。因此我們來看一下在同步器中有關同步狀態獲取和更改的三個重要方法。

getState():獲取當前同步狀態

protected final int getState() {
        return state;
}

setState():設置當前同步狀態

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

compareAndSetState():使用CAS設置當前同步狀態

protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

這裏我們可以注意到state是一個volatile類型的成員變量。可以保證state在修改的時候能夠及時被其他線程感知到。

我們繼續看獨佔式和共享式如何獲取釋放鎖

獨佔式獲取鎖

//獨佔式獲取同步狀態
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire(arg)方法可以確保在線程安全情況下獲取同步狀態。如果獲取同步狀態失敗,構造一個同步節點並通過addWaiter方法將該節點加入到同步隊列的尾部。接下來調用acquireQueued方法,使該節點以死循環的方式自旋獲取同步狀態。

注:在同步器的源碼中tryAcquire方法只拋出異常,沒有具體實現邏輯,需要自定義同步組件實現

private Node addWaiter(Node mode) {
    // 創建同步節點
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 如果tail節點不爲空,嘗試快速方式放入隊尾
    if (pred != null) {
        node.prev = pred;
        // CAS設置尾節點
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 將節點設置爲尾節點
    enq(node);
    return node;
}

enq方法設置尾節點。

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { 
            // tail爲空,初始化head節點,並將node設置爲tail節點
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // tail不爲空,將node設置爲尾節點
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

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);
                    // 設置頭節點的後繼節點爲空
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // shouldParkAfterFailedAcquire檢查狀態,設置節點爲等待狀態,因爲只有前驅節點爲頭節點時才嘗試獲取同步狀態
                // parkAndCheckInterrupt設置線程爲waiting狀態,並檢測線程是否被中斷
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 如果獲取同步狀態失敗,取消獲取
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire方法檢查節點的狀態。

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 獲取前驅節點的狀態
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            // 如果前驅節點是取消等待狀態,向前遍歷,直至節點是正常等待狀態
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            // 直至前驅節點是正常等待狀態,設置前驅節點的後繼節點爲node
            pred.next = node;
        } else {
            // CAS設置前驅節點爲SIGNAL狀態,即表示前驅節點的後繼節點爲等待狀態
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt方法設置線程爲waiting狀態,並檢測線程是否被中斷。

    private final boolean parkAndCheckInterrupt() {
        // 使線程進入waiting狀態
        LockSupport.park(this);
        // 檢測線程是否被中斷
        return Thread.interrupted();
    }

獨佔式獲取鎖的過程雖然繁瑣但十分重要,因此總結一下流程

獨佔式釋放鎖

    public final boolean release(int arg) {
        // 嘗試釋放同步狀態
        if (tryRelease(arg)) {
            Node h = head;
            // 頭節點不爲空,喚醒後繼節點
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

注:在同步器的源碼中tryRelease方法只拋出異常,沒有具體實現方法體,需要自定義同步組件實現

喚醒後繼節點

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            // 設置當前節點爲初始狀態
            compareAndSetWaitStatus(node, ws, 0);
        // 獲取節點的後繼節點
        Node s = node.next;
        // 節點爲空或者節點處於取消等待狀態
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            // 喚醒等待狀態的線程
            LockSupport.unpark(s.thread);
    }

unpark方法喚醒線程

public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

共享式獲取鎖

共享式獲取鎖與獨佔式獲取鎖的區別在於共享式允許多個線程獲取鎖。

    public final void acquireShared(int arg) {
        // 獲取同步狀態,返回值大於等於0,表示能夠獲取同步狀態
        if (tryAcquireShared(arg) < 0)
            // 未獲取同步狀態,進入自旋階段
            doAcquireShared(arg);
    }

在doAcquireShared方法中,以死循環的方式自旋,如果當前節點的前繼節點是頭節點,嘗試獲取同步狀態,如果返回值大於等於0,表示蓋茨獲取同步狀態成功並從自旋過程中退出。

    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;
                    }
                }
                // shouldParkAfterFailedAcquire檢查狀態,設置節點爲等待狀態,因爲只有前驅節點爲頭節點時才嘗試獲取同步狀態
                // parkAndCheckInterrupt設置線程爲waiting狀態,並檢測線程是否被中斷
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 如果獲取同步狀態失敗,取消獲取
            if (failed)
                cancelAcquire(node);
        }
    }

共享式釋放鎖

    public final boolean releaseShared(int arg) {
        // 釋放同步狀態
        if (tryReleaseShared(arg)) {
            // 喚醒後續處於等待狀態的節點
            doReleaseShared();
            return true;
        }
        return false;
    }

最後

同步器AbstractQueuedSynchronizer定義了許多實現同步組件的模板方式,自定義的同步組件將使用模板方法來實現自己的同步語義。所以如果想了解同步組件的機制那一定要先了解同步器的工作原理。

同步器AbstractQueuedSynchronizer本身是一個抽象類,部分方法雖然有方法體,也只是拋出異常,沒有實現邏輯。需要各個自定義同步組件實現,因此結合同步組件源碼理解同步器效果更佳。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章