Java中的管程,條件隊列,Condition以及實現一個阻塞隊列

轉自:http://blog.csdn.net/iter_zc

這篇裏面有一些基本的概念,理解概念是件有意義的事情,只有理解概念才能在面對具體問題的時候找到正確的解決思路。先看一下管程的概念

第一次在書上看到管程這個中文名稱覺得很迷糊,管程到底是個什麼東東,於是去找了英文原本對照一看,英文是Monitor,這不是監視器嗎,更加迷糊了,爲啥翻譯成管程?去百科上搜了下管程,管程的定義如下:“一個管程定義了一個數據結構和能夠併發進程所執行的一組操作,這組操作能同步進程和改變管程中的數據”。

從這個定義中可以看到管程其實和類的概念很相似,類是廣義的封裝了數據和方法,而管程不僅包含了數據和方法,並且它的方法能夠同步併發進程的操作。

  1. 數據

  2. 方法

  3. 它的方法能夠同步併發進程的操作

說白了,管程就是一個專門爲併發編程提出的概念,它表示一個對象自己維護自己的狀態,並且能夠根據自身狀態來同步併發的線程操作,而不是把這種同步的手段交給調用者來處理。舉個例子來說,有一個有界隊列,它提供了put和take方法,由於是有界,那麼問題來了:

  1. 隊列滿時,不能加入隊列,線程得等待

  2. 隊列空時,不能從隊列取元素,線程得等待

如果讓調用者自己來控制這種狀態,那麼代碼可能如下,通過不斷輪詢狀態,直到退出輪詢

        while(true){
			if(array.isFull()){
				Thread.sleep(100);
			}
		}

這種方式是非常低效並且存在問題的,因爲在併發情況下,如果不加鎖的話,狀態是難以控制的。

所以一種更好的方法是使用管程這種結構,由併發對象自己控制自己的狀態並來同步線程操作。

接下來看下條件謂詞的概念,謂詞就是動詞,表示一種動作。條件謂詞指的是檢查管程狀態的動作,比如

  1. isFull 是否滿

  2. isEmpty 是否空

條件謂詞是狀態改變操作的前提條件,需要不斷的輪詢條件謂詞直到滿足才能進行狀態改變操作。

再看條件隊列這個概率,條件隊列指的是一組在等待某個條件變成真的線程,隊列中的元素是線程。

一個條件隊列肯定和一個鎖相關聯。比較每個Java對象都有一個內置鎖,用synchronized操作可以獲得內置鎖,同樣,每個Java對象都有一個條件隊列,當需要獲得內置鎖時,併發的線程就進入了條件隊列, Object的wait(), notify(), notifyAll()操作可以操作條件隊列。

  1. wait()方法將會讓當前線程進入條件隊列等待,並且釋放鎖。 這點和Thread.sleep不一樣,Thread.sleep會讓線程睡眠,但是不釋放鎖。

    需要注意的是wait()方法的退出條件是它被notify或者notifyAll方法喚醒了,並且在又一次的鎖競爭中獲得了鎖,也就說,當wait方法退出時,當前線程還是是持有鎖的。

  2. notify()方法,從條件隊列的線程中隨即喚醒一個線程,並讓它去參與鎖競爭

  3. notifyAll()方法,喚醒條件隊列中所有的等待線程,讓它們參與鎖競爭

Java 1.5之後新增了顯式鎖的接口java.util.concurrent.locks.Lock接口,同樣提供了顯式的條件接口Condition,並對條件隊列進行了增強。

一個內置鎖只能對應一個條件隊列,這有個缺陷,就是當一個鎖對應多個條件謂詞時,多個條件謂詞只能公用一個條件隊列,這時候喚醒等待線程時有可能出現喚醒丟失的情況。比如上面有界隊列的情況,有兩個條件謂詞 isFull 和 isEmpty,當對兩個條件謂詞都進行wait()時,如果使用notify()方法來喚醒的話,只是會從條件隊列中選取一個線程,並不知道這個線程是在哪個條件謂詞上等待,這就出現了所謂的喚醒丟失的情況。所以使用內置條件隊列時,最好使用notifyAll()方法來喚醒所有的線程,避免出現喚醒丟失這個活躍性問題。但是notifyAll是一個重的方法,它會帶來大量的上下文切換和鎖競爭。

顯式鎖和顯式條件隊列避免了這個問題,一個顯示鎖可以對應多個條件Condition,一個Condition維護一個條件隊列,這樣對於多個條件謂詞,比如isFull和isEmpty,可以使用兩個Condition,對每個條件謂詞單獨await,喚醒時可以單獨signal,效率更高。

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 
    void unlock(); 
    // 創建一個條件
    Condition newCondition();
}

// Condition接口封裝了條件隊列的方法
public interface Condition {
    void await() throws InterruptedException;
    void awaitUninterruptibly();
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    boolean awaitUntil(Date deadline) throws InterruptedException;
    void signal();
    void signalAll();
}

後面會有具體的例子來比較使用內次鎖和內置條件隊列以及使用顯式鎖和顯式條件隊列的區別。

可以看到管程中能夠根據狀態同步線程操作(主要是讓線程等待)的方法的寫法有固定的流程,是個三元組: 鎖,條件謂詞,wait()方法

  1. 先獲得鎖

  2. 輪詢條件謂詞直到滿足條件

  3. 一個wait要對應一個notify或notifyAll。值得注意的是,使用wait()方法必須要先獲取鎖

對於內置鎖,寫法如下

    public synchronized void put(T item) throws InterruptedException {
	     while(isFull()){
			wait();
	     }
         ......
   	}

對應顯式鎖,寫法如下

    public void put(T item) throws InterruptedException {
	     lock.lock();
	     try {
		    while (count == array.length) {
				isFull.await();
		    }
         }finally{
            lock.unlock(); 
         }
    }

下面我們使內置鎖和內置條件隊列實現一個阻塞隊列:

  1. 有兩個條件謂詞 isFull()和 isEmpty()來判斷隊列是否滿和是否空

  2. 當put方法時,先獲取內置鎖,然後輪詢isFull()狀態,如果滿就使用內置條件隊列的wait()方法讓線程等待。

    當不滿時,wait()方法會被notify喚醒,然後競爭鎖,直到獲得鎖,進入下面的流程

    修改完狀態後,需要調用notifyAll()方法做一次喚醒操作,需要注意的時,put方法裏面的notifyAll是爲了喚醒在isEmpty條件謂詞等待的線程。但是由於一個內置鎖只能有一個條件隊列,所以notifyAll也會喚醒在isFull條件謂詞等待的線程,這樣會帶來性能的消耗。

    如果這裏使用notify()方法,就會發生喚醒丟失,因爲notify()方法只負責喚醒條件隊列的一個線程,不知道它在哪個條件謂詞等待。如果喚醒的是在isFull條件謂詞等待的線程時,就發生了喚醒丟失。

  3. take方法同put方法一樣,只是take在isEmpty條件謂詞等待,修改完狀態後,同樣需要notifyAll所有的線程來競爭鎖。

     package com.zc.lock;
    
     public class BlockingArray<T> {
     private final T[] array;
     
     
    
     private int head;
     	
     	private int tail;
     	
     	private int count;
     	
     	public BlockingArray(int size){
     		array = (T[])new Object[size];
     	} 
     	
     	public synchronized void put(T item) throws InterruptedException {
     		while(isFull()){
     			wait();
     		}
     		
     		array[tail] = item;
     		if(++ tail == array.length){
     			tail = 0;
     		}
     		count ++;
     		System.out.println("Add item: " + item);
     		// 通知條件隊列有元素進入
     		notifyAll();
     	}
     	
     	public synchronized T take() throws InterruptedException {
     		while(isEmpty()){
     			wait();
     		}
     		
     		T item = array[head];
     		if(++ head == array.length){
     			head = 0;
     		}
     		count --;
     		System.out.println("Take item: " + item);
     		// 通知條件隊列有元素出去
     		notifyAll();
     		return item;
     	}
     	
     	public synchronized boolean isFull(){
     		return count == array.length;
     	}
     	
     	public synchronized boolean isEmpty(){
     		return count == 0;
     	}
     }
    

下面有顯式鎖Lock和顯式條件Condition來實現一個阻塞隊列

  1. 定義了一個ReentrantLock顯式鎖

  2. 由這個顯式鎖創建兩個條件對應isFull條件謂詞和isEmpty條件謂詞,這兩個條件都是綁定的同一個Lock對象

  3. put方法時,先獲得顯式鎖,然後輪詢隊列是否滿,如果滿了就用Condition的await()來讓線程等待。當隊列不滿時,await()方法被signal()方法喚醒,競爭鎖直到退出await()方法。修改完狀態會,單獨對isEmpty的條件謂詞喚醒,使用isEmpty條件的signal方法單獨對在isEmpty等待的線程喚醒,這樣效率比notifyAll高很多

  4. take方法和put原理一樣

     package com.zc.lock;
    
     import java.util.concurrent.locks.Condition;
     import java.util.concurrent.locks.ReentrantLock;
    
     public class BlockingArrayWithCondition<T> {
     	private final T[] array;
    
     	private int head;
    
     	private int tail;
    
     	private int count;
    
     	private java.util.concurrent.locks.Lock lock = new ReentrantLock();
    
     	private Condition isFull = lock.newCondition();
    
     	private Condition isEmpty = lock.newCondition();
    
     	public BlockingArrayWithCondition(int size) {
     		array = (T[]) new Object[size];
     	}
    
     	public void put(T item) throws InterruptedException {
     		lock.lock();
     		try {
     			while (count == array.length) {
     				isFull.await();
     			}
    
     			array[tail] = item;
     			if (++tail == array.length) {
     				tail = 0;
     			}
     			count++;
     			System.out.println("Add item: " + item);
     			// 通知isEmpty條件隊列有元素進入
     			isEmpty.signal();
     		} finally {
     			lock.unlock();
     		}
     	}
    
     	public T take() throws InterruptedException {
     		lock.lock();
     		try {
     			while (count == 0) {
     				isEmpty.await();
     			}
    
     			T item = array[head];
     			if (++head == array.length) {
     				head = 0;
     			}
     			count--;
     			System.out.println("Take item: " + item);
     			// 通知isFull條件隊列有元素出去
     			isFull.signal();
     			return item;
     		} finally {
     				lock.unlock();
     		}
     	}
     }
    

下面我們寫一個測試用例對這個阻塞隊列進行測試

  1. 使用100個線程往阻塞隊列裏面put() 1到100的數字

  2. 使用100個線程從阻塞隊列take一個數

  3. 最後的結果應該是放入了1到100個數字,取出了1到100個數字,不會有重複數字,也不會有數字丟失

  4. 一個數肯定是先put後take

     package com.zc.lock;
    
     import java.util.concurrent.atomic.AtomicInteger;
    
     public class BlockingArrayTest {
     	public static void main(String[] args){
     		//final BlockingArray<Integer> blockingArray = new BlockingArray<Integer>(10);
     	
     		final BlockingArrayWithCondition<Integer> blockingArray = new BlockingArrayWithCondition<Integer>(10);
     	
     	
     		final AtomicInteger count = new AtomicInteger(0);
     	
     		for(int i = 0; i < 100; i ++){
     			Thread t = new Thread(new Runnable(){
     
     				@Override
     				public void run() {
     					try {
     						blockingArray.put(count.incrementAndGet());
     					} catch (InterruptedException e) {
     						e.printStackTrace();
     					}
     				}
     			
     			});
     			t.start();
     		}
     	
     		for(int i = 0; i < 100; i ++){
     			Thread t = new Thread(new Runnable(){
     
     				@Override
     				public void run() {
     					try {
     						blockingArray.take();
     					} catch (InterruptedException e) {
     						e.printStackTrace();
     					}
     				}
     			
     				});
     			t.start();
     		}
     	
     	}
     }
    

測試結果如下,證明阻塞隊列的實現是正確的:

  1. 放入了100個數,取出了100個數,沒有重複的數字,也有沒有數字丟失

  2. 數字先放入後取出

Add item: 1
Add item: 2
Add item: 3
Add item: 4
Add item: 5
Add item: 6
Add item: 7
Add item: 8
Add item: 9
Add item: 10
Take item: 1
Take item: 2
Add item: 11
Add item: 12
Take item: 3
Take item: 4
Add item: 13
Take item: 5
Take item: 6
Take item: 7
Take item: 8
Take item: 9
Add item: 14
Take item: 10
Take item: 11
Take item: 12
Add item: 15
Take item: 13
Take item: 14
Take item: 15
Add item: 16
Add item: 17
Add item: 18
Take item: 16
Take item: 17
Take item: 18
Add item: 19
Take item: 19
Add item: 20
Take item: 20
Add item: 21
Take item: 21
Add item: 22
Take item: 22
Add item: 23
Add item: 24
Take item: 23
Take item: 24
Add item: 25
Take item: 25
Add item: 26
Take item: 26
Add item: 27
Take item: 27
Add item: 28
Take item: 28
Add item: 29
Take item: 29
Add item: 30
Take item: 30
Add item: 31
Take item: 31
Add item: 32
Take item: 32
Add item: 33
Take item: 33
Add item: 34
Take item: 34
Add item: 35
Take item: 35
Add item: 36
Take item: 36
Add item: 37
Take item: 37
Add item: 38
Take item: 38
Add item: 39
Take item: 39
Add item: 40
Take item: 40
Add item: 41
Take item: 41
Add item: 42
Take item: 42
Add item: 43
Take item: 43
Add item: 44
Take item: 44
Add item: 45
Take item: 45
Add item: 46
Take item: 46
Add item: 47
Take item: 47
Add item: 48
Take item: 48
Add item: 49
Take item: 49
Add item: 50
Take item: 50
Add item: 51
Take item: 51
Add item: 52
Take item: 52
Add item: 53
Take item: 53
Add item: 54
Take item: 54
Add item: 55
Take item: 55
Add item: 56
Take item: 56
Add item: 57
Take item: 57
Add item: 58
Take item: 58
Add item: 59
Take item: 59
Add item: 60
Take item: 60
Add item: 61
Take item: 61
Add item: 62
Take item: 62
Add item: 63
Take item: 63
Add item: 64
Take item: 64
Add item: 65
Take item: 65
Add item: 66
Take item: 66
Add item: 67
Take item: 67
Add item: 68
Take item: 68
Add item: 69
Take item: 69
Add item: 70
Take item: 70
Add item: 71
Take item: 71
Add item: 72
Take item: 72
Add item: 73
Take item: 73
Add item: 74
Take item: 74
Add item: 75
Take item: 75
Add item: 76
Take item: 76
Add item: 77
Take item: 77
Add item: 78
Take item: 78
Add item: 79
Take item: 79
Add item: 80
Take item: 80
Add item: 81
Take item: 81
Add item: 82
Take item: 82
Add item: 83
Take item: 83
Add item: 84
Take item: 84
Add item: 85
Take item: 85
Add item: 86
Take item: 86
Add item: 87
Take item: 87
Add item: 88
Take item: 88
Add item: 89
Take item: 89
Add item: 90
Take item: 90
Add item: 91
Take item: 91
Add item: 92
Take item: 92
Add item: 93
Take item: 93
Add item: 94
Take item: 94
Add item: 95
Take item: 95
Add item: 96
Take item: 96
Add item: 97
Take item: 97
Add item: 98
Take item: 98
Add item: 99
Take item: 99
Add item: 100
Take item: 100

轉載請註明來源: http://blog.csdn.net/iter_zc

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