在進入實戰篇以前,我們簡單說一下多線程編程的一般原則。
[安全性]是多線程編程的首要原則,如果兩個以上的線程訪問同一對象時,一個線程會損壞另一個線程的資料,這就是違反了安全性原則,這樣的程式是不能進入實際應用的。
安全性的保證可以通過設計安全的類和程式員的手工控制。如果多個線程對同一對象訪問不會危及安全性,這樣的類就是安全執行緒的類,在JAVA中比如String類就被設計為安全執行緒的類。而如果不是安全執行緒的類,那麼就需要程式員在訪問這些類的執行個體時手工控制它的安全性。
[可行性]是多線程編程的另一個重要原則,如果僅僅實現了安全性,程式卻在某一點後不能繼續執行或者多個線程發生死結,那麼這樣的程式也不能作為真正的多線程程式來應用。
相對而言安全性和可行性是相互抵觸的,安全性越高的程式,可性行會越低。要綜合平衡。
[高效能] 多線程的目的本來就是為了增加程式啟動並執行效能,如果一個多線程完成的工作還不如單線程完成得快。那就不要應用多線程了。
高效能程式主要有以下幾個方面的因素:
資料吞吐率,在一定的時間內所能完成的處理能力。
響應速度,從發出請求到收到響應的時間。
容量,指同時處理雅緻同任務的數量。
安全性和可行性是必要條件,如果達到不這兩個原則那就不能稱為真正的多線程程式。而高性是多線程編程的目的,也可以說是充要條件。否則,為什麼採用多線程編程呢?
[生產者與消費者模式]
首先以一個生產者和消費者模式來進入實戰篇的第一節。
生產者和消費者模式中保護的是誰?
多線程編程都在保護著某些對象,這些個對象是"緊俏資源",要被最大限度地利用,這也是採用多線程方式的理由。在生產者消費者模式中,我們要保護的是"倉庫",在我下面的這個例子中,
就是桌子(table)。
我這個例子的模式完全是生產者-消費者模式,但我換了個名字。廚師-食客模式,這個食堂中只有1張桌子,同時最多放10個盤子,現在有4個廚師做菜,每做好一盤就往桌子上放(生產者將產品往倉庫中放),而有6個食客不停地吃(消費者消費產品,為了說明問題,他們的食量是無限的)。
一般而言,廚師200-400ms做出一盤菜,而食客要400-600ms吃完一盤。當桌子上放滿了10個盤子後,所有廚師都不能再往桌子上放,而當桌子是沒有盤子時,所有的食客都只好等待。
下面我們來設計這個程式:
因為我們不知道具體是什麼菜,所以叫它food:
class Food{}
然後是桌子,因為它要有序地放而且要有序地取(不能兩個食客同時爭取第三盤菜),所以我們擴充LinkedList,或者你用彙總把一個LinkedList作為屬性也能達到同樣的目的,例子中我是用
繼承,從構造方法中傳入一個可以放置的最大值。
class Table extends java.util.LinkedList{ int maxSize; public Table(int maxSize){ this.maxSize = maxSize; }}
現在我們要為它加兩個方法,一是廚師往上面放菜的方法,一是食客從桌子上拿菜的方法。
放菜:因為一張桌子由多個廚師放菜,所以廚師放菜的要被同步,如果桌子上已經有十盤菜了。所有廚師就要等待:
public synchronized void putFood(Food f){ while(this.size() >= this.maxSize){ try{ this.wait(); }catch(Exception e){} } this.add(f); notifyAll(); }
拿菜:同上面,如果桌子上一盤菜也沒有,所有食客都要等待:
public synchronized Food getFood(){ while(this.size() <= 0){ try{ this.wait(); }catch(Exception e){} } Food f = (Food)this.removeFirst(); notifyAll(); return f; }
廚師類:
由於多個廚師要往一張桌子上放菜,所以他們要操作的桌子應該是同一個對象,我們從構造方法中將桌子物件傳進去以便控制在主線程中只產生一張桌子。
廚師做菜要用一定的時候,我用在make方法中用sleep表示他要消耗和時候,用200加上200的隨機數保證時間有200-400ms中。做好後就要往桌子上放。
這裡有一個非常重要的問題一定要注意,就是對什麼範圍同步的問題,因為產生競爭的是桌子,所以所有putFood是同步的,而我們不能把廚師自己做菜的時間也放在同步中,因為做菜是各自做的。同樣食客吃菜的時候也不應該同步,只有從桌子中取菜的時候是競爭的,而具體吃的時候是各自在吃。所以廚師類的代碼如下:
class Chef extends Thread{ Table t; Random r = new Random(12345); public Chef(Table t){ this.t = t; } public void run(){ while(true){ Food f = make(); t.putFood(f); } } private Food make(){ try{ Thread.sleep(200+r.nextInt(200)); }catch(Exception e){} return new Food(); }}
同理我們產生食客類的代碼如下:
class Eater extends Thread{ Table t; Random r = new Random(54321); public Eater(Table t){ this.t = t; } public void run(){ while(true){ Food f = t.getFood(); eat(f); } } private void eat(Food f){ try{ Thread.sleep(400+r.nextInt(200)); }catch(Exception e){} }}
完整的程式在這兒:
package debug;import java.util.regex.*;import java.util.*;class Food{}class Table extends LinkedList{ int maxSize; public Table(int maxSize){ this.maxSize = maxSize; } public synchronized void putFood(Food f){ while(this.size() >= this.maxSize){ try{ this.wait(); }catch(Exception e){} } this.add(f); notifyAll(); } public synchronized Food getFood(){ while(this.size() <= 0){ try{ this.wait(); }catch(Exception e){} } Food f = (Food)this.removeFirst(); notifyAll(); return f; }}class Chef extends Thread{ Table t; String name; Random r = new Random(12345); public Chef(String name,Table t){ this.t = t; this.name = name; } public void run(){ while(true){ Food f = make(); System.out.println(name+" put a Food:"+f); t.putFood(f); } } private Food make(){ try{ Thread.sleep(200+r.nextInt(200)); }catch(Exception e){} return new Food(); }}class Eater extends Thread{ Table t; String name; Random r = new Random(54321); public Eater(String name,Table t){ this.t = t; this.name = name; } public void run(){ while(true){ Food f = t.getFood(); System.out.println(name+" get a Food:"+f); eat(f); } } private void eat(Food f){ try{ Thread.sleep(400+r.nextInt(200)); }catch(Exception e){} }}public class Test { public static void main(String[] args) throws Exception{ Table t = new Table(10); new Chef("Chef1",t).start(); new Chef("Chef2",t).start(); new Chef("Chef3",t).start(); new Chef("Chef4",t).start(); new Eater("Eater1",t).start(); new Eater("Eater2",t).start(); new Eater("Eater3",t).start(); new Eater("Eater4",t).start(); new Eater("Eater5",t).start(); new Eater("Eater6",t).start(); }}
這一個例子中,我們主要關注以下幾個方面:
1.同步方法要保護的對象,本例中是保護桌子,不能同時往上放菜或同時取菜。
假如我們把putFood方法和getFood方法在廚師類和食客類中實現,那麼我們應該如此:
(以putFood為例)
class Chef extends Thread{ Table t; String name; public Chef(String name,Table t){ this.t = t; this.name = name; } public void run(){ while(true){ Food f = make(); System.out.println(name+" put a Food:"+f); putFood(f); } } private Food make(){ Random r = new Random(200); try{ Thread.sleep(200+r.nextInt()); }catch(Exception e){} return new Food(); } public void putFood(Food f){//方法本身不能同步,因為它同步的是this.即Chef的執行個體 synchronized (t) {//要保護的是t while (t.size() >= t.maxSize) { try { t.wait(); } catch (Exception e) {} } t.add(f); t.notifyAll(); } }}
2.同步的範圍,在本例中是放和取兩個方法,不能把做菜和吃菜這種各自不相干的工作放在受保護的範圍中。
3.參與者與容積比
對於生產者和消費者的比例,以及桌子所能放置最多菜的數量三者之間的關係是影響效能的重要因素,如果是過多的生產者在等待,則要增加消費者或減少生產者的資料,反之則增加生產者或減少消費者的數量。
另外如果桌子有足夠的容量可以很大程式提升效能,這種情況下可以同時提高生產者和消費者的數量,但足夠大的容時往往你要有足夠大的實體記憶體。
轉載自dev2dev網友axman的go deep into java專欄。