依賴狀態的執行緒任務的等待方式之忙等待
引入:
有時候我們執行一個操作,需要一個前提條件,只有在條件滿足的情況下,才能繼續執行。在單執行緒程式中,如果某個狀態變數不滿足條件,則基本上可以直接返回。但是,在併發程式中,基於狀態的條件可能會由於其他執行緒的操作而改變。而且存在這種需要,即某個操作一定要完成,如果當前條件不滿足,沒關係,我可以等,等到條件滿足的時候再執行。今天,我們就來 聊一聊等待的幾種方式。
- 忙等待 / 自旋等待。
- 讓權等待 / 輪詢與休眠
- 條件佇列
情景條件
我們要實現一個有界快取,其中 用不同的等待方式處理前提條件失敗的問題 。在每種實現中都擴充套件了BaseBoundedBuffer,這個類中實現了一個基於陣列的迴圈快取,其中各個快取狀態變數(buf、head、tail和count)均由快取的內建鎖來保護。它還提供了同步的doPut和doTake方法,並在子類中通過這些方法來實現put和take操作,底層的狀態將對子類隱藏。
此段程式碼來自《Java Concurrency in Practice》
public abstract class BaseBoundedBuffer<V> { private final V[] buf;//緩衝陣列 private int tail;//緩衝資料尾部索引 private int head;//頭部索引 private int count;//儲存的資料量 public BaseBoundedBuffer(int capacity) { this.buf = (V[]) new Object[capacity]; } 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; } }
忙等待:反覆檢查條件是否為真,直到條件達到,繼而完成後續任務。
我們來看看,忙等待的實現方式:
public class BusyWaitBoundedBuffer<V> extends BaseBoundedBuffer<V> { public BusyWaitBoundedBuffer(int size) { super(size); } public void put(V v) throws InterruptedException { while(true) { synchronized (this) { if(!isFull()) { doPut(v); return; } } } } public V take() throws InterruptedException { while(true) { synchronized (this) { if(!isEmpty()) return doTake(); } } } }
這裡的兩個方法在訪問快取時都採用" 先檢查,再執行 "的邏輯策略,非執行緒安全,因為條件可能在" 檢查之後,執行之前 "的中間時刻,被其他執行緒修改,以至於,在執行的時候,前提條件已經不滿足了,故需要對put和take兩個方法都進行同步,共用同一個鎖以確保實現對緩衝狀態的 獨佔訪問 ,即某一時刻只能有一個執行緒可以訪問操作緩衝陣列。也就是說,在put方法執行的一次嘗試中,take方法不能被呼叫,不能改變緩衝陣列狀態。
還有一點,值得注意的是,while迴圈並不在同步塊內,而是同步塊在while迴圈內,也就是每執行一次條件檢查,如果不滿足,需要釋放掉鎖。不然另一個方法就拿不到鎖,也就不能改變狀態,條件就永遠不能發生改變,這個方法就變成了 死等待 。
這樣做,雖然在邏輯上實現了功能要求,但是在效能上卻可能消耗過多的CPU時間,因為它佔據著CPU,不做計算,而只是等待。
“尚未解決的疑惑”: 執行緒等待鎖的時候是否會被JVM掛起,調出CPU?如果是這樣的話,那麼上下文切換的開銷也會很大,因為每檢查一次條件,需要進出CPU兩次。