多線程&JVM&鎖相關—深入學習java同步器AQS

介紹:

AQS(AbstractQueuedSynchronizer類)是一個用來構建鎖和同步器的框架,它在內部定義了一個int state變量,用來表示同步狀態.在LOCK包中的相關鎖(常用的有ReentrantLock、 ReadWriteLock)都是基於AQS來構建.然而這些鎖都沒有直接來繼承AQS,而是定義了一個Sync類去繼承AQS.那麼爲什麼要這樣呢?because:鎖面向的是使用用戶,而同步器面向的則是線程控制,那麼在鎖的實現中聚合同步器而不是直接繼承AQS就可以很好的隔離二者所關注的事情.


AQS是通過一個雙向的FIFO 同步隊列來完成同步狀態的管理,當有線程獲取鎖失敗後,就被添加到隊列末尾,讓我看一下這個隊列

紅色節點爲頭結點,可以把它當做正在持有鎖的節點,


public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    static final class Node {...}
    private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;//同步狀態

由上可知,它把head和tail設置爲了volatile,這兩個節點的修改將會被其他線程看到,事實上,我們也主要是通過修改這兩個節點來完成入隊和出隊.接下來一起我們一起學習Node


static final class Node {
    //該等待同步的節點處於共享模式
    static final Node SHARED = new Node();
    //該等待同步的節點處於獨佔模式
    static final Node EXCLUSIVE = null;

    //等待狀態,這個和state是不一樣的:有1,0,-1,-2,-3五個值
    volatile int waitStatus;
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;

    volatile Node prev;//前驅節點
    volatile Node next;//後繼節點
    volatile Thread thread;//等待鎖的線程
    //和節點是否共享有關
    Node nextWaiter;
    //Returns true if node is waiting in shared mode
    final boolean isShared() {
            return nextWaiter == SHARED;
        }

下面解釋下waitStatus五個的得含義:

  • CANCELLED(1):該節點的線程可能由於超時或被中斷而處於被取消(作廢)狀態,一旦處於這個狀態,節點狀態將一直處於CANCELLED(作廢),因此應該從隊列中移除.
  • SIGNAL(-1):當前節點爲SIGNAL時,後繼節點會被掛起,因此在當前節點釋放鎖或被取消之後必須被喚醒(unparking)其後繼結點.
  • CONDITION(-2) 該節點的線程處於等待條件狀態,不會被當作是同步隊列上的節點,直到被喚醒(signal),設置其值爲0,重新進入阻塞狀態.
  • 0:新加入的節點
在鎖的獲取時,並不一定只有一個線程才能持有這個鎖(或者稱爲同步狀態),所以此時有了獨佔模式和共享模式的區別,也就是在Node節點中由nextWait來標識。比如ReentrantLock就是一個獨佔鎖,只能有一個線程獲得鎖,而WriteAndReadLock的讀鎖則能由多個線程同時獲取,但它的寫鎖則只能由一個線程持有。這次先介紹獨佔模式下鎖(或者稱爲同步狀態)的獲取與釋放.這個類使用到了模板方法設計模式:定義一個操作中算法的骨架,而將一些步驟的實現延遲到子類中。

1. 獨佔模式同步狀態的獲取


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

該方法首先嚐試獲取鎖( tryAcquire(arg)的具體實現定義在了子類中),如果獲取到,則執行完畢,否則通過addWaiter(Node.EXCLUSIVE), arg)方法把當前節點添加到等待隊列末尾,並設置爲獨佔模式,


private Node addWaiter(Node mode) {
        //把當前線程包裝爲node,設爲獨佔模式
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        //如果tail不爲空,把node插入末尾
        if (pred != null) {
            node.prev = pred;
            //此時可能有其他線程插入,所以重新判斷tail
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //此時可能有其他線程插入,所以重新判斷tail是否爲空
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

如果tail節點爲空,執行enq(node);重新嘗試,最終把node插入.在把node插入隊列末尾後,它並不立即掛起該節點中線程,因爲在插入它的過程中,前面的線程可能已經執行完成,所以它會先進行自旋操作acquireQueued(node, arg),嘗試讓該線程重新獲取鎖!當條件滿足獲取到了鎖則可以從自旋過程中退出,否則繼續。


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);
        }
    }

如果沒獲取到鎖,則判斷是否應該掛起,而這個判斷則得通過它的前驅節點的waitStatus來確定:


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);
        pred.next = node;
    } else {       
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
return false;
}

如果前驅節點的waitStatus爲:

  • SIGNAL,則返回true表示應該掛起當前線程,掛起該線程,並等待被喚醒,被喚醒後進行中斷檢測,如果發現當前線程被中斷,那麼拋出InterruptedException並退出循環.
  • >0,將前驅節點踢出隊列,返回false
  • <0,也是返回false,不過先將前驅節點waitStatus設置爲SIGNAL,使得下次判斷時,將當前節點掛起.
最後,我們對獲取獨佔式鎖過程對做個總結:

AQS的模板方法acquire通過調用子類自定義實現的tryAcquire獲取同步狀態失敗後->將線程構造成Node節點(addWaiter)->將Node節點添加到同步隊列對尾(addWaiter)->節點以自旋的方法獲取同步狀態(acquirQueued)。在節點自旋獲取同步狀態時,只有其前驅節點是頭節點的時候纔會嘗試獲取同步狀態,如果該節點的前驅不是頭節點或者該節點的前驅節點是頭節點單獲取同步狀態失敗,則判斷當前線程需要阻塞,如果需要阻塞則需要被喚醒過後才返回。

2. 獨佔模式同步狀態的釋放

既然是釋放,那肯定是持有鎖的該線程執行釋放操作,即head節點中的線程釋放鎖.

AQS中的release釋放同步狀態和acquire獲取同步狀態一樣,都是模板方法,tryRelease釋放的具體操作都有子類去實現,父類AQS只提供一個算法骨架。


public final boolean release(int arg) {
if (tryRelease(arg)) {
        Node h = head;
if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
return true;
    }
return false;
}
/**如果node的後繼節點不爲空且不是作廢狀態,則喚醒這個後繼節點,否則從末尾開始尋找合適的節點,如果找到,則喚醒*/
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);
    }

過程:首先調用子類的tryRelease()方法釋放鎖,然後喚醒後繼節點,在喚醒的過程中,需要判斷後繼節點是否滿足情況,如果後繼節點不爲且不是作廢狀態,則喚醒這個後繼節點,否則從tail節點向前尋找合適的節點,如果找到,則喚醒.

綜上,我們已經描述完了獨佔鎖的獲取和釋放,至於共享鎖的操作,有時間會再聊,請持續關注!


發佈了125 篇原創文章 · 獲贊 127 · 訪問量 53萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章