Java併發編程基礎---(9)Java中的鎖---隊列同步器

寫在前面:

    在這一篇博客中,將會介紹Java中的鎖等相關內容,篇幅較長,會循序漸進的總結從隊列同步器再到Lock接口的相關實現類。

隊列同步器:

    隊列同步器 (AbstractQueuedSynchronizer) ,是用來構建鎖和其他同步組件的基礎,從Java API中,我們可以看出,在AbstractQueuedSynchronizer類中,定義了一個int 類型的變量-來記錄同步的狀態,(如果state=1則是獲取了同步狀態,=0則表示沒有獲取到),定義一個內部類Node--來實現一個雙向隊列(FIFO)。

    private volatile int state;
    static final class Node {}

    隊列同步器,主要是通過子類來繼承隊列同步器的一些方法。會重寫隊列同步器中獨佔/共享,獲取釋放同步狀態的方法。

	    protected boolean tryAcquire(int arg) {
	    	//獨佔式獲取同步狀態
	        throw new UnsupportedOperationException();
	    }
	    protected boolean tryRelease(int arg) {
	    	//獨佔式釋放同步狀態
	        throw new UnsupportedOperationException();
	    }
	    protected int tryAcquireShared(int arg) {
	    	//共享式獲取同步狀態
	        throw new UnsupportedOperationException();
	    }
	    protected boolean tryReleaseShared(int arg) {
	    	//共享式釋放同步狀態
	        throw new UnsupportedOperationException();
	    }
	    protected boolean isHeldExclusively() {
	    	//當前同步器是否在獨佔模式下被線程佔用
	        throw new UnsupportedOperationException();
	    }

 

    AbstractQueuedSynchronizer的設計師基於模板方法設計模式,使用者需要基礎並重寫同步器的指定方法。此外,在類中,提供了幾個返回同步狀態的方法,來方便子類修改線程的同步狀態。

 

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

隊列同步器的實現:

1. 同步隊列

    同步器依賴內部的同步隊列來完成同步狀態的管理。當前線程獲取同步狀態時,同步器會把當前線程及等待狀態等信息構造成一個節點(Node),並加入同步隊列。同時會阻塞當前線程,當同步狀態釋放時,會喚醒同步隊列中首節點,使其能夠再次獲得同步狀態。(同步狀態即獲取到鎖)。

    同步隊列器中提供的是雙向隊列,且在同步隊列器中提供了首節點(head)和尾節點(tail),沒有獲取同步狀態的線程,將會被加入到隊列的尾部。設置尾部節點的方法時上文中提到的:

    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

    同步器提供的基於CAS(compareAndSwap)的設置尾節點的方法。方法的參數需要傳遞兩個參數,一個爲當前線之前的尾部節點和當前節點。

    此外,因爲同步隊列遵循先進先出原則,頭節點是獲取同步狀態成功的節點。當首節點在釋放同步狀態時,將會喚醒後繼節點,而後繼節點在獲取同步狀態成功後,將自己設置爲首節點。

2. 獨佔式同步狀態的獲取與釋放

    通過acquire方法可以獲取到獨佔式同步狀態,方法實現代碼如下:

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

    上述代碼中:

(1) 首先調用tryAcquire方法保證線程安全的情況下去獲取同步狀態

(2) 如果獲取失敗,則構造同步節點(獨佔式NODE.EXCLUSIVE)

(3) 然後通過addWaiter方法加入到同步隊列的尾部

(4) 最後調用acquireQueued方法以死循環的方式獲取同步狀態

下面是實現過程的源碼(JDK8):

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

    addWaiter中通過compareAndSetTail來確保節點能夠被線程安全添加。如果使用Linkedlist來維持一個節點之間關係,由於linkedlist很難保證Node的正確添加和順序不亂。

    private Node enq(final Node node) {
        for (;;) {
            Node t = 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;
                }
            }
        }
    }

    enq方法中,用了一個for的死循環來保證節點的正確添加,enq方法實際上是將併發添加節點的程序,通過compareAndSetTail中的CAS,將節點的添加串行化解決。

注:CAS,即compareAndSwap,是由sun包下的Unsafe類實現。而Unsafe類中的方法都是native方法,由JVM本地實現。

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

    acquiredQueued方法會不斷地嘗試獲取同步狀態,但是隻要前驅節點才能嘗試獲取同步狀態,因爲:

    (1) 因爲在頭部節點在成功獲取到同步狀態的結點,在他釋放同步狀態之後,將會喚醒他的後繼節點。

    (2) 維護FIFO的原則。

    自旋過程:是指前驅節點爲頭節點且能夠獲取同步狀態的判斷條件和線程進入等待狀態。

    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方法釋放同步狀態,同時喚醒頭節點的後繼節點。

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

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

    通過調用acquiredShared方法就可以共享地,即多線程同時獲取同步狀態,在acquiredShared方法中又調用了doAcquiredShared方法。

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

    與獨佔式獲取同步狀態一樣,獲取到之後,會釋放掉同步狀態,與與獨佔式不同的是,共享式必須保證線程安全,因爲在釋放同步狀態的同事要操作多個線程。

    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }

4. 獨享式超時獲取同步狀態

    通過調用doAcquireNanos方法可以超時獲取同步狀態,即在指定時間內獲取同步狀態。獲取到返回true,未獲取到返回false。    

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

 

 

 

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