學習阻塞隊列BlockingQueue

如果BlockQueue是空的,從BlockingQueue取東西的操作將會被阻斷進入等待狀態,直到BlockingQueue進了東西纔會被喚醒.同樣,如果BlockingQueue是滿的,任何試圖往裏存東西的操作也會被阻斷進入等待狀態,直到BlockingQueue裏有空間纔會被喚醒繼續操作.

1.BlockingQueue定義的常用方法如下

1)add(anObject)
把anObject加到BlockingQueue裏,即如果BlockingQueue可以容納,則返回true,否則報異常

2)offer(anObject)
表示如果可能的話,將anObject加到BlockingQueue裏,即如果BlockingQueue可以容納,則返回true,否則返回false.

3)put(anObject)
把anObject加到BlockingQueue裏,如果BlockQueue沒有空間,則調用此方法的線程被阻斷直到BlockingQueue裏面有空間再繼續.

4)poll(time)
取走BlockingQueue裏排在首位的對象,若不能立即取出,則可以等time參數規定的時間,取不到時返回null

5)take()
取走BlockingQueue裏排在首位的對象,若BlockingQueue爲空,阻斷進入等待狀態直到Blocking有新的對象被加入爲止

6)drainTo()
一次性從BlockingQueue獲取所有可用的數據對象(還可以指定獲取數據的個數), 通過該方法,可以提升獲取數據效率;不需要多次分批加鎖或釋放鎖。

- Throws Exception Special Value Blocks Times Out
Insert add(o) offer(o) put(o) offer(o, timeout, timeunit)
Remove remove(o) poll() take() poll(timeout, timeunit)
Examine element() peek()

這四套方法對應的特點分別是:

1. ThrowsException:如果操作不能馬上進行,則拋出異常
2. SpecialValue:如果操作不能馬上進行,將會返回一個特殊的值,一般是true或者false
3. Blocks:如果操作不能馬上進行,操作會被阻塞
4. TimesOut:如果操作不能馬上進行,操作會被阻塞指定的時間,如果指定時間沒執行,則返回一個特殊值,一般是true或者false

2.BlockingQueue具體的實現類,根據不同需求,選擇不同的實現類

1)ArrayBlockingQueue
規定大小的BlockingQueue,其構造函數必須帶一個int參數來指明其大小.其所含的對象是以FIFO(先入先出)順序排序的.

BlockingQueue queue = new ArrayBlockingQueue(1024);
queue.put("1");
Object object = queue.take();

2)LinkedBlockingQueue
大小不定的BlockingQueue,若其構造函數帶一個規定大小的參數,生成的BlockingQueue有大小限制,若不帶大小參數,所生成的BlockingQueue的大小由Integer.MAX_VALUE來決定.

和ArrayBlockingQueue一樣,LinkedBlockingQueue 也是以先進先出的方式存儲數據,最新插入的對象是尾部,最新移出的對象是頭部。下面是一個初始化和使LinkedBlockingQueue的例子:

BlockingQueue<String> unbounded = new LinkedBlockingQueue<String>();
BlockingQueue<String> bounded   = new LinkedBlockingQueue<String>(1024);
bounded.put("Value");
String value = bounded.take();

ArrayBlockingQueue和LinkedBlockingQueue是兩個最普通也是最常用的阻塞隊列,一般情況下,在處理多線程間的生產者消費者問題,使用這兩個類足以。

生產者消費者實例:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

public class BlockingQueueTest {

    public static void main(String[] args) throws InterruptedException {
        // 聲明一個容量爲10的緩存隊列
        BlockingQueue<String> queue = new LinkedBlockingQueue<String>(10);

        Producer producer1 = new Producer(queue);
        Producer producer2 = new Producer(queue);
        Producer producer3 = new Producer(queue);
        Consumer consumer = new Consumer(queue);

        // 藉助Executors
        ExecutorService service = Executors.newCachedThreadPool();
        // 啓動線程
        service.execute(producer1);
        service.execute(producer2);
        service.execute(producer3);
        service.execute(consumer);

        // 執行10s
        Thread.sleep(10 * 1000);
        producer1.stop();
        producer2.stop();
        producer3.stop();

        Thread.sleep(2000);
        // 退出Executor
        service.shutdown();
    }
}
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 生產者
 */
public class Producer implements Runnable {
    private volatile boolean isRunning = true;
    private BlockingQueue queue;
    private static AtomicInteger count = new AtomicInteger();
    private static final int DEFAULT_RANGE_FOR_SLEEP = 1000;

    public Producer(BlockingQueue queue) {
        this.queue = queue;
    }

    public void run() {
        String data = null;
        Random r = new Random();

        System.out.println("啓動生產者線程!");
        try {
            while (isRunning) {
                System.out.println("正在生產數據...");
                Thread.sleep(r.nextInt(DEFAULT_RANGE_FOR_SLEEP));

                data = "data:" + count.incrementAndGet();
                System.out.println("將數據:" + data + "放入隊列...");
                if (!queue.offer(data, 2, TimeUnit.SECONDS)) {
                    System.out.println("放入數據失敗:" + data);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        } finally {
            System.out.println("退出生產者線程!");
        }
    }

    public void stop() {
        isRunning = false;
    }
}
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * 消費者
 */
public class Consumer implements Runnable {
    private BlockingQueue<String> queue;
    private static final int DEFAULT_RANGE_FOR_SLEEP = 1000;

    public Consumer(BlockingQueue<String> queue) {
        this.queue = queue;
    }

    public void run() {
        System.out.println("啓動消費者線程!");
        Random r = new Random();
        boolean isRunning = true;
        try {
            while (isRunning) {
                System.out.println("正從隊列獲取數據...");
                String data = queue.poll(2, TimeUnit.SECONDS);
                if (null != data) {
                    System.out.println("拿到數據:" + data);
                    System.out.println("正在消費數據:" + data);
                    Thread.sleep(r.nextInt(DEFAULT_RANGE_FOR_SLEEP));
                } else {
                    // 超過2s還沒數據,認爲所有生產線程都已經退出,自動退出消費線程。
                    isRunning = false;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        } finally {
            System.out.println("退出消費者線程!");
        }
    }
}

3)DelayQueue
DelayQueue中的元素只有當其指定的延遲時間到了,才能夠從隊列中獲取到該元素。DelayQueue是一個沒有大小限制的隊列,因此往隊列中插入數據的操作(生產者)永遠不會被阻塞,而只有獲取數據的操作(消費者)纔會被阻塞。

使用場景:DelayQueue使用場景較少,但都相當巧妙,常見的例子比如使用一個DelayQueue來管理一個超時未響應的連接隊列。
  
DelayQueue阻塞的是其內部元素,DelayQueue中的元素必須實現 java.util.concurrent.Delayed接口,這個接口的定義非常簡單:

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}

getDelay()方法的返回值就是隊列元素被釋放前的保持時間,如果返回0或者一個負值,就意味着該元素已經到期需要被釋放,此時DelayedQueue會通過其take()方法釋放此對象。

從上面Delayed 接口定義可以看到,它還繼承了Comparable接口,這是因爲DelayedQueue中的元素需要進行排序,一般情況,我們都是按元素過期時間的優先級進行排序。

例1:爲一個對象指定過期時間

首先,我們先定義一個元素,這個元素要實現Delayed接口

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class DelayedElement implements Delayed {
      //過期時間
      private long expired;
      private long delay;
      private String name;

      public DelayedElement(String elementName, long delay) {
             this.name = elementName;
             this.delay= delay;
             this.expired = ( delay + System. currentTimeMillis());
      }

      @Override
      public int compareTo(Delayed o) {
            //因爲比較的DelayedElement類,所以必須先強制類型轉換。
            DelayedElement cached=(DelayedElement) o;
            //DelayedQueue中的元素需要進行排序,一般情況,我們都是按元素過期時間的優先級進行排序。
            return cached.getExpired() > this.expired ? 1 : -1;
      }

      @Override
      public long getDelay(TimeUnit unit) {
             //過期時間減當前時間 (如果當前時間 到達 過期時間,該元素被釋放)
             return ( expired - System. currentTimeMillis());
      }

      @Override
      public String toString() {
             return "DelayedElement [delay=" + delay + ", name=" + name + "]";
      }

      public long getExpired() {
             return expired;
      }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {
        DelayQueue<DelayedElement> queue = new DelayQueue<>();
        //設置這個元素的過期時間爲3s
        DelayedElement ele = new DelayedElement("cache 3 seconds", 3000);
        queue.put(ele);
        System.out.println(queue.take());
    }
}

運行這個main函數,我們可以發現,我們需要等待3s之後纔會打印這個對象(toString())。

DelayedElement [delay=3000, name=cache 3 seconds]

其實DelayQueue應用場景很多,比如定時關閉連接、緩存對象,超時處理等各種場景,下面我們就拿學生考試爲例讓大家更深入的理解DelayQueue的使用。

例2:把所有考試的學生看做是一個DelayQueue,誰先做完題目釋放誰

首先,我們構造一個學生對象

public class Student implements Runnable, Delayed {
    private String name; // 姓名
    private long costTime;// 做試題的時間
    private long finishedTime;// 完成時間

    public Student(String name, long costTime) {
        this.name = name;
        this.costTime = costTime;
        finishedTime = costTime + System.currentTimeMillis();
    }

    @Override
    public void run() {
        System.out.println(name + " 交卷,用時" + costTime / 1000);
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return (finishedTime - System.currentTimeMillis());
    }

    @Override
    public int compareTo(Delayed o) {
        Student other = (Student) o;
        return costTime >= other.costTime ? 1 : -1;
    }

}

然後在構造一個教師對象對學生進行考試

public class Teacher {
    static final int STUDENT_SIZE = 30;

    public static void main(String[] args) throws InterruptedException {
        Random r = new Random();
        // 把所有學生看做一個延遲隊列
        DelayQueue<Student> students = new DelayQueue<Student>();
        // 構造一個線程池用來讓學生們“做作業”
        ExecutorService exec = Executors.newFixedThreadPool(STUDENT_SIZE);
        for (int i = 0; i < STUDENT_SIZE; i++) {
            // 初始化學生的姓名和做題時間
            students.put(new Student("學生" + (i + 1), 3000 + r.nextInt(10000)));
        }
        // 開始做題
        while (!students.isEmpty()) {
            exec.execute(students.take());
        }
        exec.shutdown();
    }
}

我們看一下運行結果:

學生4 交卷,用時3
學生13 交卷,用時4
學生5 交卷,用時5
學生14 交卷,用時5
學生2 交卷,用時5
學生3 交卷,用時6
學生26 交卷,用時6

通過運行結果我們可以發現,每個學生在指定開始時間到達之後就會“交卷”(取決於getDelay()方法),並且是先做完的先交卷(取決於compareTo()方法)。

4)PriorityBlockingQueue優先級對列
4.1 PriorityBlockingQueue是一個沒有邊界的隊列,它的排序規則和 java.util.PriorityQueue一樣。需要注意,PriorityBlockingQueue中允許插入null對象。

基於優先級的阻塞隊列(優先級的判斷通過構造函數傳入的Compator對象來決定),但需要注意的是PriorityBlockingQueue並不會阻塞數據生產者,而只會在沒有可消費的數據時,阻塞數據的消費者。因此使用的時候要特別注意,生產者生產數據的速度絕對不能快於消費者消費數據的速度,否則時間一長,會最終耗盡所有的可用堆內存空間。在實現PriorityBlockingQueue時,內部控制線程同步的鎖採用的是公平鎖。

4.2 所有插入PriorityBlockingQueue的對象必須實現 java.lang.Comparable接口,隊列優先級的排序規則就是按照我們對這個接口的實現來定義的。

規則是:當前和其他對象比較,如果compare方法返回負數,那麼在隊列裏面的優先級就比較高。

4.3 另外,我們可以從PriorityBlockingQueue獲得一個迭代器Iterator,但這個迭代器並不保證按照優先級順序進行迭代。

問題:有沒有人在使用時,發現將添加在PriorityBlockingQueue的一系列元素打印出來,隊列的元素其實並不是全部按優先級排序的,但是隊列頭的優先級肯定是最高的?
回覆:PriorityBlockingQueue隊列添加新元素時候不是將全部元素進行順序排列。但是每取一個頭元素時候,都會對剩餘的元素做一次調整,這樣就能保證每次隊列頭的元素都是優先級最高的元素

下面我們舉個例子來說明一下,首先我們定義一個對象類型,這個對象需要實現Comparable接口:

public class PriorityElement implements Comparable<PriorityElement> {
    private int priority;// 定義優先級

    public PriorityElement(int priority) {
        // 初始化優先級
        this.priority = priority;
    }

    @Override
    public int compareTo(PriorityElement o) {
        // 按照優先級大小進行排序
        return priority >= o.getPriority() ? 1 : -1;
    }

    public int getPriority() {
        return priority;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    @Override
    public String toString() {
        return "PriorityElement [priority=" + priority + "]";
    }
}

然後我們把這些元素隨機設置優先級放入隊列中

public class PriorityBlockingQueueExample {
    public static void main(String[] args) throws InterruptedException {
        PriorityBlockingQueue<PriorityElement> queue = new PriorityBlockingQueue<>();
        for (int i = 0; i < 5; i++) {
            Random random = new Random();
            PriorityElement ele = new PriorityElement(random.nextInt(10));
            queue.put(ele);
        }
        while (!queue.isEmpty()) {
            System.out.println(queue.take());
        }
    }
}

看一下運行結果:

PriorityElement [priority=3]
PriorityElement [priority=5]
PriorityElement [priority=7]
PriorityElement [priority=7]
PriorityElement [priority=8]

5)SynchronousQueue
特殊的BlockingQueue,對其的操作必須是放和取交替完成的. 隊列內部僅允許容納一個元素。當一個線程插入一個元素後會被阻塞,除非這個元素被另一個線程消費。

一種無緩衝的等待隊列,類似於無中介的直接交易,有點像原始社會中的生產者和消費者,生產者拿着產品去集市銷售給產品的最終消費者,而消費者必須親自去集市找到所要商品的直接生產者,如果一方沒有找到合適的目標,那麼對不起,大家都在集市等待。相對於有緩衝的BlockingQueue來說,少了一箇中間經銷商的環節(緩衝區),如果有經銷商,生產者直接把產品批發給經銷商,而無需在意經銷商最終會將這些產品賣給那些消費者,由於經銷商可以庫存一部分商品,因此相對於直接交易模式,總體來說採用中間經銷商的模式會吞吐量高一些(可以批量買賣);但另一方面,又因爲經銷商的引入,使得產品從生產者到消費者中間增加了額外的交易環節,單個產品的及時響應性能可能會降低。
  聲明一個SynchronousQueue有兩種不同的方式,它們之間有着不太一樣的行爲。公平模式和非公平模式的區別:
  如果採用公平模式:SynchronousQueue會採用公平鎖,並配合一個FIFO隊列來阻塞多餘的生產者和消費者,從而體系整體的公平策略;
  但如果是非公平模式(SynchronousQueue默認):SynchronousQueue採用非公平鎖,同時配合一個LIFO隊列來管理多餘的生產者和消費者,而後一種模式,如果生產者和消費者的處理速度有差距,則很容易出現飢渴的情況,即可能有某些生產者或者是消費者的數據永遠都得不到處理。

學習鏈接:
http://blog.csdn.net/suifeng3051/article/details/48807423
http://www.cnblogs.com/liuling/p/2013-8-20-01.html
http://wsmajunfeng.iteye.com/blog/1629354
http://blog.sina.com.cn/s/blog_6145ed8101010q1y.html

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