接上篇文章 獨佔式同步狀態擷取與釋放
通過調用同步器的acquire(int arg)方法可以擷取同步狀態,該方法對中斷不敏感,也就是由於線程擷取同步狀態失敗後進入同步隊列中,後續對線程進行中斷操作時,線程不會從同步隊列中移出,程式碼範例:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();}
上述代碼主要完成了同步狀態擷取、節點構造、加入同步隊列以及在同步隊列中自旋等待的相關工作,其主要邏輯是:首先調用自訂同步器實現的tryAcquire(int arg)方法,該方法保證安全執行緒的擷取同步狀態,如果同步狀態擷取失敗,則構造同步節點(獨佔式Node.EXCLUSIVE,同一時刻只能有一個線程成功擷取同步狀態)並通過addWaiter(Node node)方法將該節點加入到同步隊列的尾部,最後調用acquireQueued(Node node,int arg)方法,使得該節點以“死迴圈”的方式擷取同步狀態。如果擷取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞線程被中斷來實現
下面我們看下同步器的addWriter和enq方法
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) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
上述代碼通過使用compareAndSetTail(Node expect,Node update)方法來確保節點能夠被安全執行緒添加。試想一下:如果使用一個普通的LinkedList來維護節點之間的關係,那麼當一個線程擷取了同步狀態,而其他多個線程由於調用tryAcquire(int arg)方法擷取同步狀態失敗而並發地被添加到LinkedList時,LinkedList將難以保證Node的正確添加,最終的結果可能是節點的數量有偏差,而且順序也是混亂的。在enq(final Node node)方法中,同步器通過“死迴圈”來保證節點的正確添加,在“死迴圈”中只有通過CAS將節點設定成為尾節點之後,當前線程才能從該方法返回,否則,當前線
程不斷地嘗試設定。可以看出,enq(final Node node方法將並發添加節點的請求通過CAS變得“序列化”了。節點進入同步隊列之後,就進入了一個自旋的過程,每個節點(或者說每個線程)都在自省地觀察,當條件滿足,擷取到了同步狀態,就可以從這個自旋過程中退出,否則依舊留在這個自旋過程中(並會阻塞節點的線程)
我們來看看同步器中的acquireQueue方法
/** * Acquires in exclusive uninterruptible mode for thread already in * queue. Used by condition wait methods as well as acquire. * * @param node the node * @param arg the acquire argument * @return {@code true} if interrupted while waiting */ 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); } }
在acquireQueued(final Node node,int arg)方法中,當前線程在“死迴圈”中嘗試擷取同步狀態,而只有前驅節點是前端節點才能夠嘗試擷取同步狀態,因為
1、前端節點是成功擷取到同步狀態的節點,而前端節點的線程釋放了同步狀態之後,將會喚醒其後繼節點,後繼節點的線程被喚醒後需要檢查自己的前驅節點是否是前端節點。
2、維護同步隊列的FIFO原則。該方法中,節點自旋擷取同步狀態的行為如下圖所示
上圖中由於非首節點線程前驅節點出隊或者被中斷而從等待狀態返回,隨後檢查自己的前驅是否是前端節點,如果是則嘗試擷取同步狀態。可以看到節點和節點之間在迴圈檢查的過程中基本不相互連信,而是簡單地判斷自己的前驅是否為前端節點,這樣就使得節點的釋放規則符合FIFO,並且也便於對過早通知的處理(過早通知是指前驅節點不是前端節點的線程由於中斷而被喚醒)。
獨佔式同步狀態擷取流程,也就是acquire(int arg)方法調用流程,如下圖所示
前驅節點為前端節點且能夠擷取同步狀態的判斷條件和線程進入等待狀態是擷取同步狀態的自旋過程。當同步狀態擷取成功之後,當前線程從acquire(int arg)方法返回,如果對於鎖這種並發組件而言,代表著當前線程擷取了鎖
當前線程擷取同步狀態並執行了相應邏輯之後,就需要釋放同步狀態,使得後續節點能夠繼續擷取同步狀態。通過調用同步器的release(int arg)方法可以釋放同步狀態,該方法在釋放了同步狀態之後,會喚醒其後繼節點(進而使後繼節點重新嘗試擷取同步狀態)
同步器的釋放代碼如下:
/** * Releases in exclusive mode. Implemented by unblocking one or * more threads if {@link #tryRelease} returns true. * This method can be used to implement method {@link Lock#unlock}. * * @param arg the release argument. This value is conveyed to * {@link #tryRelease} but is otherwise uninterpreted and * can represent anything you like. * @return the value returned from {@link #tryRelease} */ 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(int arg)方法釋放同步狀態,然後喚醒前端節點的後繼節點 共用式同步狀態的擷取與釋放
共用式擷取與獨佔式擷取最主要的區別在於同一時刻能否有多個線程同時擷取到同步狀態。以檔案的讀寫為例,如果一個程式在對檔案進行讀操作,那麼這一時刻對於該檔案的寫操作均被阻塞,而讀操作能夠同時進行。寫操作要求對資源的獨佔式訪問,而讀操作可以是共用式訪問,兩種不同的訪問模式在同一時刻對檔案或資源的訪問情況。
通過調用同步器的acquireShared(int arg)方法可以共用式地擷取同步狀態,我們看下同步器的acquireShared和doAcquireShared方法
/** * Acquires in shared mode, ignoring interrupts. Implemented by * first invoking at least once {@link #tryAcquireShared}, * returning on success. Otherwise the thread is queued, possibly * repeatedly blocking and unblocking, invoking {@link * #tryAcquireShared} until success. * * @param arg the acquire argument. This value is conveyed to * {@link #tryAcquireShared} but is otherwise uninterpreted * and can represent anything you like. */ public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); } /** * Acquires in shared uninterruptible mode. * @param arg the acquire argument */ 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); } }
註:為何JDK原理的死迴圈都是for(;;)而不是while(1),因為while(1)編譯之後是mov eax,1 test eax,eax je foo+23h jmp foo+18h,for(;;)編譯之後是jmp foo+23h,可以看出for(;;)指令少,不佔用寄存器,沒有判斷跳轉,效率更高
在acquireShared(int arg)方法中,同步器調用tryAcquireShared(int arg)方法嘗試擷取同步狀態,tryAcquireShared(int arg)方法傳回值為int類型,當傳回值大於等於0時,表示能夠擷取到同步狀態。因此,在共用式擷取的自旋過程中,成功擷取到同步狀態並退出自旋的條件就是
tryAcquireShared(int arg)方法傳回值大於等於0。可以看到,在doAcquireShared(int arg方法的自旋過程中,如果當前節點的前驅為前端節點時,嘗試擷取同步狀態,如果傳回值大於等於0,表示該次擷取同步狀態成功並從自旋過程中退出。與獨佔式一樣,共用式擷取也需要釋放同步狀態,通過調用releaseShared(int arg)方法可以釋放同步狀態,該方法代碼如下
/** * Releases in shared mode. Implemented by unblocking one or more * threads if {@link #tryReleaseShared} returns true. * * @param arg the release argument. This value is conveyed to * {@link #tryReleaseShared} but is otherwise uninterpreted * and can represent anything you like. * @return the value returned from {@link #tryReleaseShared} */ public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
該方法在釋放同步狀態之後,將會喚醒後續處於等待狀態的節點。對於能夠支援多個線程同時訪問的並發組件(比如Semaphore),它和獨佔式主要區別在於tryReleaseShared(int arg)方法必須確保同步狀態(或者資源數)安全執行緒釋放,一般是通過迴圈和CAS來保證的,因為釋放同步狀態的操作會同時來自多個線程。 獨佔式逾時過去同步狀態
通過調用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以逾時擷取同步狀態,即在指定的時間段內擷取同步狀態,如果擷取到同步狀態則返回true,否則,返回false。
在Java 5之前,當一個線程擷取不到鎖而被阻塞在synchronized之外時,對該線程進行中斷操作,此時該線程的中斷標誌位會被修改,但線程依舊會阻塞在synchronized上,等待著擷取鎖。在Java 5中,同步器提供了acquireInterruptibly(int arg)方法,這個方法在等待擷取同步狀態時,如果當前線程被中斷,會立刻返回,並拋出InterruptedException
逾時擷取同步狀態過程可以被視作響應中斷擷取同步狀態過程的“增強版”,doAcquireNanos(int arg,long nanosTimeout)方法在支援響應中斷的基礎上,增加了逾時擷取的特性。針對逾時擷取,主要需要計算出需要睡眠的時間間隔nanosTimeout,為了防止過早通知,
nanosTimeout計算公式為:nanosTimeout-=now-lastTime,其中now為當前喚醒時間,lastTime為上次喚醒時間,如果nanosTimeout大於0則表示逾時時間未到,需要繼續睡眠nanosTimeout納秒,反之,表示已經逾時,該方法代碼如下
/** * Acquires in exclusive timed mode. * * @param arg the acquire argument * @param nanosTimeout max wait time * @return {@code true} if acquired */ private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { long lastTime = System.nanoTime(); 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; } if (nanosTimeout <= 0) return false; if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); long now = System.nanoTime(); nanosTimeout -= now - lastTime; lastTime = now; if (Thread.interrupted()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
該方法在自旋過程中,當節點的前驅節點為前端節點時嘗試擷取同步狀態,如果擷取成功則從該方法返回,這個過程和獨佔式同步擷取的過程類似,但是在同步狀態擷取失敗的處理上有所不同。如果當前線程擷取同步狀態失敗,則判斷是否逾時(nanosTimeout小於等於0表示
已經逾時),如果沒有逾時,重新計算逾時間隔nanosTimeout,然後使當前線程等待nanosTimeout納秒(當已到設定的逾時時間,該線程會從LockSuport.parkNanos(Object blocker,long nanos)方法返回)。如果nanosTimeout小於等於spinForTimeoutThreshold(1000納秒)時,將不會使該線程進行逾時等待,而是進入快速的自旋過程。原因在於,非常短的逾時等待無法做到十分精確,如果這時再進行逾時等待,相反會讓nanosTimeout的逾時從整體上表現得反而不精確。因此,在逾時非常短的情境下,同步器會進入無條件的快速自旋。
獨佔逾時擷取同步狀態的流程如下:
自訂同步群組件
設計一個同步工具:該工具在同一時刻,只允許至多兩個線程同時訪問,超過兩個線程的訪問將被阻塞,我們將這個同步工具命名為TwinsLock。
首先,確定訪問模式。TwinsLock能夠在同一時刻支援多個線程的訪問,這顯然是共用式訪問,因此,需要使用同步器提供的acquireShared(int args)方法等和Shared相關的方法,這就要求TwinsLock必須重寫tryAcquireShared(int args)方法和tryReleaseShared(int args)方法,這樣才能保證同步器的共用式同步狀態的擷取與釋放方法得以執行。
其次,定義資源數。TwinsLock在同一時刻允許至多兩個線程的同時訪問,表明同步資源數為2,這樣可以設定初始狀態status為2,當一個線程進行擷取,status減1,該線程釋放,則status加1,狀態的合法範圍為0、1和2,其中0表示當前已經有兩個線程擷取了同步資源,此時再有其他線程對同步狀態進行擷取,該線程只能被阻塞。在同步狀態變更時,需要使用compareAndSet(int expect,int update)方法做原子性保障。
最後,組合自訂同步器。
public class TwinsLock implements Lock { private final Sync sync = new Sync(2); @SuppressWarnings("serial") private static final class Sync extends AbstractQueuedSynchronizer{ Sync(int count){ if(count <0){ throw new IllegalArgumentException("count must larger than zero"); } setState(count); } public int tryAcquireShared(int reduceCount){ for(;;){ int current = getState(); int newCount = current - reduceCount; if(newCount < 0 || compareAndSetState(current, newCount)){ return newCount; } } } public boolean tryReleaseShared(int returnCount){ for(;;){ int current = getState(); int newCount = current+returnCount; if(compareAndSetState(current, newCount)){ return true; } } } } @Override public void lock() { sync.acquireShared(1); } @Override public void lockInterruptibly() throws InterruptedException { // TODO Auto-generated method stub } @Override public boolean tryLock() { // TODO Auto-generated method stub return false; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { // TODO Auto-generated method stub return false; } @Override public void unlock() { sync.tryReleaseShared(1); } @Override public Condition newCondition() { // TODO Auto-generated method stub return null; }}
測試類別
public class TwinsLockTest { public static void main(String[] args) { final Lock lock = new TwinsLock(); class Worker extends Thread{ public void run(){ for(;;){ lock.lock(); try { Thread.sleep(1000); System.out.println(Thread.currentThread().getName()); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }finally{ lock.unlock(); } } } } for(int i=0;i<10;i++){ Worker worker = new Worker(); worker.setDaemon(true); worker.start(); } for(int i=0;i<10;i++){ try { Thread.sleep(1000); System.out.println("----"); } catch (InterruptedException e) { e.printStackTrace(); } } }}
運行結果如下:
Thread-0Thread-1------------Thread-1Thread-0--------Thread-1Thread-0--------Thread-0Thread-1----Thread-1Thread-0--------
如果限制改成5,結果如下:
Thread-2Thread-3Thread-0----Thread-1Thread-4--------Thread-3Thread-2Thread-0Thread-4Thread-1--------Thread-0Thread-3Thread-4Thread-2Thread-1----Thread-4Thread-3Thread-2Thread-0----
由於並發的情況不同,線程數不同,但是都在5以內