編寫代碼實現以下功能
子線程迴圈10次,接著主線程迴圈100次,接著又回到子線程迴圈10次,接著再回到主線程又迴圈100次,如此迴圈50次。
分析
1)子線程迴圈10次與主線程迴圈100次必須是互斥的執行,不能出現交叉,下面代碼中通過synchronized關鍵字實現此要求;
2)子線程與主線程必須交替出現,可以通過線程同步通訊技術實現,下面代碼中通過bShouldSub變數實現此要求;
其他需要注意的地方
1)其中business變數必須聲明為final類型,因為在匿名內部類和局部內部類中調用的局部變數必須是final的,這樣保證:
- 變數的一致性(編譯時間final變數會被複製一份作為局部內部類的變數);
- 並避免局部變數的生命週期與局部內部類的對象的生命週期不一致。
否則,
- 若該變數被傳入局部內部類之後,局部變數在外部類中被修改,則內部類中該變數的值與外部類中不一致,可能導致不可預知的情況發生;
- 或是導致局部變數生命週期隨著方法的結束而從棧中清除,局部內部類訪問一個已不存在的變數。
若是成員變數,則不需要是final的。
2)內部類分為成員內部類、靜態內部類、局部內部類、匿名內部類四種,四者的生命週期及詳細使用方法請自行問Google或度娘。
代碼實現
public class TraditionalThreadCommunication { public static void main(String[] args) { // 必須聲明為final final Business business = new Business(); new Thread( new Runnable() { @Override public void run() { for(int i=1; i<=50; i++) { business.sub(i); } } } ).start(); for(int i=1; i<=50; i++) { business.main(i); } }}class Business { // 該變數用於線程間通訊 private boolean bShouldSub = true; public synchronized void sub(int i) { if(!bShouldSub) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } for(int j=1; j<=10; j++) { System.out.println("sub thread sequence of " + j + ", loop of " + i); } bShouldSub = false; this.notify(); } public synchronized void main(int i) { if(bShouldSub) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } for(int j=1; j<=100; j++) { System.out.println("main thread sequence of " + j + ", loop of " + i); } bShouldSub = true; this.notify(); }}
Java並發——線程間通訊與同步技術
本文會介紹有界緩衝的概念與實現,在一步步實現有界緩衝的過程中引入線程間通訊與同步技術的必要性。首先先介紹一個有界緩衝的抽象基類,所有具體實現都將繼承自這個抽象基類:
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]; } 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; }}
在向有界緩衝中插入或者提取元素時有個問題,那就是如果緩衝已滿還需要插入嗎?如果緩衝為空白,提取的元素又是什嗎?以下幾種具體實現將分別回答這個問題。
1、將異常傳遞給調用者
最簡單的實現方式是:如果緩衝已滿,向緩衝中添加元素,我們就拋出異常:
public class GrumpyBoundedBuffer<V> extends BaseBoundedBuffer<V> { public GrumpyBoundedBuffer() { this(100); } public GrumpyBoundedBuffer(int size) { super(size); } public synchronized void put(V v) throws BufferFullException { if (isFull()) throw new BufferFullException(); doPut(v); } public synchronized V take() throws BufferEmptyException { if (isEmpty()) throw new BufferEmptyException(); return doTake(); }}
這種方法實現簡單,但是使用起來卻不簡單,因為每次put()與take()時都必須準備好捕捉異常,這或許滿足某些需求,但是有些人還是希望插入時檢測到已滿的話,可以阻塞在那裡,等隊列不滿時插入對象。
2、通過輪詢與休眠實現簡單的阻塞
當隊列已滿插入資料時,我們可以不拋出異常,而是讓線程休眠一段時間,然後重試,此時可能隊列已經不是已滿狀態:
public class SleepyBoundedBuffer<V> extends BaseBoundedBuffer<V> { int SLEEP_GRANULARITY = 60; public SleepyBoundedBuffer() { this(100); } public SleepyBoundedBuffer(int size) { super(size); } public void put(V v) throws InterruptedException { while (true) { synchronized (this) { if (!isFull()) { doPut(v); return; } } Thread.sleep(SLEEP_GRANULARITY); } } public V take() throws InterruptedException { while (true) { synchronized (this) { if (!isEmpty()) return doTake(); } Thread.sleep(SLEEP_GRANULARITY); } }}
這種實現方式最大的問題是,我們很難確定合適的休眠間隔,如果休眠間隔過長,那麼程式的響應性會變差,如果休眠間隔過短,那麼會浪費大量CPU時間。
3、使用條件隊列實現有界緩衝
使用休眠的方式會有響應性問題,因為我們無法保證當隊列為非滿狀態時線程就會立刻sleep結束並且檢測到,所以,我們希望能有另一種實現方式,當緩衝非滿時,會主動喚醒線程,而不是需要線程去輪詢緩衝狀態,Object對象上的wait()與notifyAll()能夠實現這個需求。當調用wait()方法時,線程會自動釋放鎖,並請求請求作業系統掛起當前線程;當其他線程檢測到條件滿足時,會調用notifyAll()方法喚醒掛起
public class BoundedBuffer<V> extends BaseBoundedBuffer<V> { public BoundedBuffer() { this(100); } public BoundedBuffer(int size) { super(size); } public synchronized void put(V v) throws InterruptedException { while (isFull()) wait(); doPut(v); notifyAll(); } public synchronized V take() throws InterruptedException { while (isEmpty()) wait(); V v = doTake(); notifyAll(); return v; } public synchronized void alternatePut(V v) throws InterruptedException { while (isFull()) wait(); boolean wasEmpty = isEmpty(); doPut(v); if (wasEmpty) notifyAll(); }}
注意,上面的例子中我們使用了notifyAll()喚醒線程而不是notify()喚醒線程,如果我們改用notify()喚醒線程的話,將導致錯誤的,notify()會在等待隊列中隨機播放一個線程喚醒,而notifyAll()會喚醒所有等待線程。對於上面的例子,如果現在是非滿狀態,我們使用notify()喚醒線程,由於只能喚醒一個線程,那麼我們喚醒的可能是在等待非空狀態的線程,將導致訊號丟失。只有同時滿足以下兩個條件時,才能用單一的notify而不是notifyAll:
所有等待線程的類型都相同。只有一個條件謂詞與條件隊列相關,並且每個線程在從wait返回後將執行相同的操作。
單進單出。在條件變數上的每次通知,最多隻能喚醒一個線程來執行。
4、使用顯示的Condition實現有界緩衝
內建條件隊列存在一些缺陷,每個內建鎖都只能有一個相關聯的條件隊列,因而像上個例子,多個線程都要在同一個條件隊列上等待不同的條件謂詞,如果想編寫一個帶有多個條件謂詞的並發對象,就可以使用顯示的鎖和Condition,與內建鎖不同的是,每個顯示鎖可以有任意數量的Condition對象。以下代碼給出了有界緩衝的另一種實現,即使用兩個Condition,分別為notFull和notEmpty,用於表示"非滿"與"非空"兩個條件謂詞。
public class ConditionBoundedBuffer<T> { protected final Lock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); private static final int BUFFER_SIZE = 100; private final T[] items = (T[]) new Object[BUFFER_SIZE]; private int tail, head, count; public void put(T x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[tail] = x; if (++tail == items.length) tail = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } } public T take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); T x = items[head]; items[head] = null; if (++head == items.length) head = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } }}
注意,在上面的例子中,由於使用了兩個Condition對象,我們的喚醒方法調用的是signal()方法,而不是signalAll()方法。
使用條件隊列時,需要特別注意鎖、條件謂詞和條件變數之間的三元關係:在條件謂詞中包含的變數必須由鎖保護,在檢查條件謂詞以及調用wait和notify(或者await和signal)時,必須持有鎖對象。