提出問題:為啥要有雙緩沖隊列?
引用09年9月《程序員》上的一句話:雙緩沖隊列就是沖著同步/互斥的開銷來的。我們知道,在多個線程并發訪問同一個資源的時候,需要特別注意線程的同步問題。稍稍不注意,哦活,程序結果不正確了。最經典的就是“銀行取錢”的例子,想想,都跟現金掛上鉤了,看來這真不容忽視。
今天我們要談的不是如何去給資源加鎖解鎖來解決同步問題,今天的重點在于,如何將線程同步的開銷降低到我們力所能及的程度。如果你覺得,你可以通過增加硬件資源來彌補程序開銷,那么,你將不可能成為一個優秀的程序員。
進入正題,先引入一個例子,兩個實體:一個是玩具工廠,它的工作就是不停地生產玩具;另外一個實體就是小孩,它的工作就是不停地從工廠拿玩具。小孩不可能直接到工廠去“拿”玩具吧?呵呵,媽媽是絕對不會放心的。所以,我們有一個“搬運工”,搬運工自然要具備“存放”的功能,不然他怎么將玩具帶給小孩呢,是吧。所以,我們先將搬運工定義為一個List,用來存放工廠生產出來的玩具。
代碼如下
玩具類,定義一個玩具實體
public class Toy {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
接下來是玩具工廠,它得“不停地”生產,所以,把它設計成一個線程類
玩具工廠,將玩具對象,放到list中
public class Factory extends Thread{
public void run(){
while(true){
Toy t = new Toy ();
t.setName("玩具");
synchronized (Tools.lT){
Tools.lT.add(t);
}
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
注意到,在上面這段代碼當中,必須得用synchronized (Tools.lT)將Tools.lT給加鎖。不然會出現線程同步問題(開銷就在這里)。
再接下來,看看小孩是怎么“玩”玩具的:
小孩類,從list中取出玩具對象
public class Kid extends Thread {
long time1 = System.currentTimeMillis();
int count = 0;
public void run() {
while(true){
synchronized(Tools.lT){
if(Tools.lT.size()!=0)
Tools.lT.remove(0);
}
count++;
if(count==100000){
javax.swing.JOptionPane.showMessageDialog(null, "用時間: "+(System.currentTimeMillis()-time1));
System.exit(0);
}
}
}
}
當list不為空的時候,將list中的玩具對象,一個一個取出來,玩完!這個小孩可真夠厲害的,呵呵。可以想象為,該類的工作就是不停地向list中取出玩具對象。OK,再來編寫方法,如下
主方法
public class MainTest {
/**
* @param args
*/
public static void main(String[] args) {
Factory f = new Factory();
f.start();
Kid k = new Kid();
k.start();
}
}
最后是Tools類,他里面有個靜態的List對象:
Tools類
public class Tools {
public static List<Toy>lT = new ArrayList<Toy>(10000);
}
這樣,我們運行一下主方法,看看我們這位“天才”玩完100000個玩具,得花銷多少的時間。

好,31毫秒。
這是我們的第一種解決方法,下面再來看第二種解決方法:
其實我們在Factory類和Kid類中都進行了同步處理,這樣一來,浪費了很多時間,到底是不是這樣的呢?我們可不可以直接用一個不用處理線程同步的容器來放Toy類對象呢?這樣以來是不是就可以節省很多開銷了?這個想法是有道理的,但是,事實是不是這樣的呢?馬上實踐!
代碼就不具體貼出來了,只是我們在Tools類中用到的是一個如下的對象
Public static LinkedBlockingQueue<Toy> lT= new LinkedBlockingQueue<Toy>(1000);
對,阻塞隊列,這樣我們就只管往里面取,從里面拿了,不用自己考慮同步問題,Factory類和Kid類中也不同特意去加關鍵字進行同步了。
那么這種方案的結果是多少呢?同樣是100000個玩具,看結果

哦哦,變成16毫秒了,著實提高了不少效果呢。看來,在處理同步的時候擠時間,是有發展空間的,呵呵。
等等,有人要發話了,你在這磨嘰了半天,還是沒有說什么是雙緩沖啊,對!有了前面的兩種方案,我們再來看看“雙緩沖隊列”。
所謂雙緩沖隊列,自然起碼要有兩個隊列吧,呵呵,在這個例子中,我們可以設計兩個List來存放工廠生產出來的玩具對象。
下面分析一下:
兩個List,一個用來存,一個用來取。有點迷糊?就是有一個listP從工廠那里得到玩具對象,另外一個listT就專門把它得到的玩具對象送去給 Kid類處理。當listT變成空的了以后,再將listP中在這段時間內取到的所有玩具對象放到listT中,好,這完了之后,他們兩個就又各自干各自的去了:listP再去取,listT再去送。這樣是不是就減少了很多次的線程同步呢?至少,在它們交換之前,listP是完全被工廠類線程占有,listT是完全被Kid類線程占有的,不用處理同步。只有在listT放完了,沒得給了,再去跟ListP換過來,這個時候就要處理同步了。
跟實際聯系一下,有兩個搬運工A,B,A在工廠,專門從工廠取玩具;B在小孩子身邊,專門送玩具給小孩玩。當B身上沒有玩具了,自然要回A那里,把A身上的玩具全部拿過來,再來送給小孩玩。在A還有玩具的時候,A和B是在各自的線程里被處理的,即A在拿,B在給。不用擔心同步問題。
這樣以來,處理同步問題的次數是不是大大減少了呢?沒錯,就是這樣的。那么怎么跟代碼結合呢?
我們要設計一個監視線程,監視listP是不是空了,要是空了,把它同步起來,把listT也同步起來,讓他們交換。完了就各自干各自的了。
我們來看看這個監視類:
public class DoubleBufferList {
private List<Object> lP;
private List<Object> lT;
private int gap;
/**
* 構造方法
*
* @param lP
* 用來存放對象的隊列
* @param lT
* 用來取對象的隊列
* @param gap
* 交換的間隔
*/
public DoubleBufferList(List lP, List lT, int gap) {
this.lP = lP;
this.lT = lT;
this.gap = gap;
}
public void check() {
Runnable runner = new Runnable() {
public void run() {
while (true) {
if (lT.size() == 0) {
synchronized (lT) {
synchronized (lP) {
lT.addAll(lP);
}
lP.clear();
}
}
try {
Thread.sleep(gap);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread = new Thread(runner);
thread.start();
}
}
這個類中的線程方法就是用來交換的,目的就是為了減少處理同步的次數。這種方案中,Facotry類和Kid類跟前面單個List的情況差不多,就不再給出了。只是有一點要注意,Facory類中將玩具對象是放到了lP中,而Kid類中,也只是從lT中去取對象。看看Tools類中,多了一個變量:
Tools類,聲明兩個隊列
public static List<Toy>lT = new ArrayList<Toy>(10000);
public static List<Toy>lP = new ArrayList<Toy>(10000);
同樣是讓我們的“天才”玩完100000個玩具,來看看運行需要的時間:

哈哈,似乎跟我們上面的第二種方案,單阻塞隊列,沒有太大的差異。怎么解釋呢?
不用著急,來,我將額定的玩具量后多加個“0”,讓他玩完1000000個!改一下單阻塞隊列方案的輸出結果,給他們一個標記。再來看看結果:


效果出來了吧,我們再加大量,讓他們同時處理10000000個玩具對象:


充分說明,使用雙緩沖隊列,比單緩沖阻塞隊列的效果要好,更別說單緩沖隊列了。
總結:
從上面的分析,我們可以得知,在處理線程同步的時候,是要花費我們的時間的,雖然在有些時候,這樣的花費是我們可以接受的,但是在很多情況下,如果我們能注意到這樣的浪費,并且及時地完善我們的程序,這樣可以更大限度地提高我們程序的運行效率。尤其是在大的程序里面,這樣的效果體現得更明顯。而往往越大的系統,對性能的要求也就越高。