類別庫本身包含了許多存在狀態依賴性的類別。如FutureTask,BlockingQueue等。這些類別中的一些操作,會基於狀態的前提條件。例如,不能從一個空的佇列刪除元素或取得一個尚未結束的任務的計算結果。這兩個操作執行之前,必須等到佇列進入非空狀態或任務進入已完成狀態。我們創建狀態依賴類別最簡單的方法是在類別庫的基礎上進行建構。但如果類別庫沒有你想要的功能,那麼還可以利用Java語言和類別庫提供的底層機制來建構自己的同步機制。
所以,本篇要介紹如何去建構一個自己的狀態依賴類別。從最簡單的構造一步一步介紹到複雜的規範的構造,從而了解這個過程,知道是如何得到最後的結果。
可阻塞的狀態依賴操作如下偽代碼所示:
acquire lock on object state //首先获取锁 while (precondition does not hold) { //前提条件是否满足,不满足则一直循环重试 release lock //释放锁 wait until precondition might hold //等待知道满足前提条件 optionally fail if interrupted or timeout expire //中断或者超时,各种异常 reacquire lock //重新获取锁 } perform action //执行任务 release lock //释放锁
取得鎖定,檢查條件是否滿足,如果不滿足,則釋放鎖進入阻塞狀態,直到條件滿足或中斷、逾時等,重新取得鎖。執行任務,釋放鎖。
現在看這個偽代碼可能還不能夠直觀的理解,沒事,往下看,看完這篇文章就知道他的意思了,每個操作都是這個偽代碼架構構造的。
ArrayBlockingQueue是一個有界快取,提供的兩個操作,put 和 take。它們都包含一個前提條件:不能將元素放入到已滿的快取中,不能從空緩存中取得元素。恩,我們的目標就是建構這樣一個ArrayBlockingQueue。
接下來,介紹2種有界快取的實現,它們採用不同的方法來處理前提條件不滿足的情況。
首先,來看下面一個基底類別 BaseBoundeBuffer, 後面的實作都會擴充這個基底類別。它是一個基於數組的循環緩存,包含的變數 buf、head、tail、count都由快取的內建鎖定保護。它還提供了同步的 doPut 和 doTake 方法,並在子類別中,透過這些方法來實作 put 和 take 操作,底層的狀態將對子類別隱藏。
public abstract class BaseBoundedBuffer<V> { private final V[] buf; private int tail; private int head; private int count; protected BaseBoundedBuffer(int capacity) { this.buf = (V[]) new Object[capacity]; count = 0; } protected synchronized final void doPut(V v) { buf[tail] = v; if(++tail == buf.length) tail = 0; ++count; } protected synchronized final V doTake() { V v = buf[head]; buf[head] = null; if(++head == buf.length) head = 0; --count; return v; } public synchronized final boolean isFull() { return count == buf.length; } public synchronized final boolean isEmpty() { return count == 0; } }
第一種有界快取的實現,對 put 和 take 方法都進行同步,先檢查後執行,失敗則拋出例外。
public class GrumpyBoundedBuffer<V> extends BaseBoundedBuffer{ protected GrumpyBoundedBuffer(int capacity) { super(capacity); } public synchronized void put(V v) throws BufferFullException { if(isFull()) { throw new BufferFullException(); } doPut(v); } public synchronized V take() throws BufferFullException { if(isEmpty()) throw new BufferFullException(); return (V) doTake(); } }
如上所示,對於前提條件不滿足的情況,都直接拋出異常,這裡所謂的異常,是指緩存滿或空。實際上來講,這異常不代表程序出錯,打個比方,看到紅燈並不意味著信號燈出現了異常,而是等待直到綠燈在過馬路。所以,這裡的意思是要求在呼叫方捕獲異常,並且每次快取操作時都需要重試。
我們直接來看下面的客戶端呼叫程式碼:
private static GrumpyBoundedBuffer gbb = new GrumpyBoundedBuffer(5); ...while(true) { try { V item = gbb.take(); break; } catch(BufferEmptyException e) { Thread.sleep(500); } }
說白了就是在不滿足前提條件的情況下,再試一次,直到條件滿足,讓看起來能夠達到阻塞的效果。但是這種情況,呼叫者必須自行處理前提條件是失敗的情況,並且一直佔用CPU。這裡的問題是呼叫者使用這個佇列會很麻煩!
第二種方法,SleepyBoundedBuffer 透過輪詢和休眠來實現簡單的阻塞的重試機制,從而使得呼叫者剝離了重試機制,簡化了對快取的使用。請看下面的程式碼清單:
public class SleepyBoundedBuffer<V> extends BaseBoundedBuffer{ protected SleepyBoundedBuffer(int capacity) { super(capacity); // TODO Auto-generated constructor stub } public void put(V v) throws InterruptedException { while(true) { synchronized(this) { if(!isFull()) { doPut(v); return; } } Thread.sleep(200); } } public V take() throws InterruptedException{ while(true) { synchronized(this) { if(!isEmpty()) { return (V) doTake(); } } Thread.sleep(200); } } }
從呼叫者的角度看,這種方法可以很好的運作。假如某個操作滿足前提條件,則立即執行,否則就阻塞。呼叫者無需處理失敗和重試,但是呼叫者仍然需要處理InterruptedException。與大多數具備良好行為的阻塞庫方法一樣,SleepyBoundedBuffer 透過中斷來支援取消。
SleepyBoundedBuffer的問題在於,睡眠時間設定多長才是合理的?如何才能達到效能的最優?如下圖所示,B執行緒設定條件為真的,但此時A仍在睡眠,這個睡眠就是效能的瓶頸所在了。
恩,有沒有某種方法可以達到,當條件為真時,執行緒立刻醒過來執行呢?
賣個關子,下一篇為你解說!
以上是JAVA開發實戰狀態依賴性的管理之阻塞隊列的實現的詳細內容。更多資訊請關注PHP中文網其他相關文章!