程式扎記: [ Java 文章收集 ] Java 世界裡的 Program,Process,Thread

標籤

2011年2月23日 星期三

[ Java 文章收集 ] Java 世界裡的 Program,Process,Thread


轉載自 這裡
前言 :
在介紹Thread之前,我們必須先把Program和Process這兩個觀念作一個釐清 :
- Program:一群程式碼的集合,用以解決特定的問題。以物件導向的觀念來類比,相當於Class。
- Process:由Program所產生的執行個體,一個Program可以同時執行多次,產生多個Process。以物件導向的觀念來類比,相當於Object。每一個Process又由以下兩個東西組成
一個Memory Space。相當於Object的variable,不同Process的Memory Space也不同,彼此看不到對方的Memory Space。
一個以上的Thread。Thread代表從某個起始點開始(例如main),到目前為止所有函數的呼叫路徑,以及這些呼叫路徑上所用到的區域變數。當然程式的執行狀態,除了紀錄在主記憶體外,CPU內部的暫存器(如Program Counter, Stack Pointer, Program Status Word等)也需要一起紀錄。所以Thread又由下面兩項組成
* Stack : 紀錄函數呼叫路徑,以及這些函數所用到的區域變數
* 目前CPU的狀態

由上面的描述中,我們在歸納Thread的重點如下 :
1) 一個Process可以有多個Thread.
2) 同一個Process內的Thread使用相同的Memory Space,但這些Thread各自擁有其Stack。換句話說,Thread能透過reference存取到相同的Object,但是local variable卻是各自獨立的.
3) 作業系統會根據Thread的優先權以及已經用掉的CPU時間,在不同的Thread作切換,以讓各個Thread都有機會執行.

如何產生Thread :
Java以 java.lang.Thread 這個類別來表示Thread。Class Thread其中有兩個Constructor :
1. Thread()
2. Thread(Runnable)

使用Thread()產生的Thread,其進入點為Thread裡的run();使用Thread(Runnable)產生的Thread,其進入點為 Runnable 物件裡的run()。當run()結束時,這個Thread也就結束了;這和main()結束有相同的效果。其用法以下面範例說明 :
  1. public class ThreadExample1 extends Thread {  
  2.     public void run() { // override Thread's run()  
  3.         System.out.println("Here is the starting point of Thread.");  
  4.         for (;;) { // infinite loop to print message  
  5.             System.out.println("User Created Thread");  
  6.         }  
  7.     }  
  8.     public static void main(String[] argv) {  
  9.         Thread t = new ThreadExample1(); // 產生Thread物件  
  10.         t.start(); // 開始執行t.run()  
  11.         for (;;) {  
  12.             System.out.println("Main Thread");  
  13.         }  
  14.     }  
  15. }  
以上程式執行後,螢幕上會持續印出"User Created Thread"或"Main Thread"的字樣。利用Runnable的寫法如下 :
  1. public class ThreadExample2 implements Runnable {  
  2.     public void run() { // implements Runnable run()  
  3.         System.out.println("Here is the starting point of Thread.");  
  4.         for (;;) { // infinite loop to print message  
  5.             System.out.println("User Created Thread");  
  6.         }  
  7.     }  
  8.     public static void main(String[] argv) {  
  9.         Thread t = new Thread(new ThreadExample2()); // 產生Thread物件  
  10.         t.start(); // 開始執行Runnable.run();  
  11.         for (;;) {  
  12.             System.out.println("Main Thread");  
  13.         }  
  14.     }  
  15. }  
Thread的優先權與影響資源的相關方法 :
Thread.setPriority(int)可以設定Thread的優先權,數字越大優先權越高。Thread定義了3個相關的static final variable :
  1. public static final int MAX_PRIORITY 10  
  2. public static final int MIN_PRIORITY 1  
  3. public static final int NORM_PRIORITY 5  
要提醒讀者的是,優先權高的Thread其佔有CPU的機會比較高,但優先權低的也都會有機會執行到。其他有關Thread執行的方法有 :
yield() : 先讓給別的Thread執行
sleep(int time) : 休息time mini second(1/1000秒)
join() : 呼叫ThreadA.join()的執行緒會等到ThreadA結束後,才能繼續執行.
你可以執行下面的程式,看看yield()的效果 :
- 範例代碼 : Thread yield demo
  1. public class ThreadExample1 extends Thread {  
  2.     public void run() { // overwrite Thread's run()  
  3.         System.out.println("Here is the starting point of Thread.");  
  4.         for (;;) { // infinite loop to print message  
  5.             System.out.println("User Created Thread");  
  6.             yield();  
  7.         }  
  8.     }  
  9.     public static void main(String[] argv) {  
  10.         Thread t = new ThreadExample1(); // 產生Thread物件  
  11.         t.start(); // 開始執行t.run()  
  12.         for (;;) {  
  13.             System.out.println("Main Thread");  
  14.             yield();  
  15.         }  
  16.     }  
  17. }  

觀看join的效果 :
- 範例代碼 : Thread join demo
  1. public class JoinExample extends Thread {  
  2.     String myId;  
  3.     public JoinExample(String id) {  
  4.         myId = id;  
  5.     }  
  6.     public void run() { // overwrite Thread's run()  
  7.     for (int i=0; i < 500; i++) {  
  8.             System.out.println(myId+" Thread");  
  9.         }  
  10.     }  
  11.     public static void main(String[] argv) {  
  12.         Thread t1 = new JoinExample("T1"); // 產生Thread物件  
  13.         Thread t2 = new JoinExample("T2"); // 產生Thread物件  
  14.         t1.start(); // 開始執行t1.run()  
  15.         t2.start();  
  16.         try {  
  17.             t1.join(); // 等待t1結束  
  18.             t2.join(); // 等待t2結束  
  19.         } catch (InterruptedException e) {}  
  20.         for (int i=0;i < 5; i++) {  
  21.             System.out.println("Main Thread");  
  22.         }  
  23.     }  
  24. }  

Critical Section(關鍵時刻)的保護措施 :
如果設計者沒有提供保護機制的話,Thread取得和失去CPU控制權的時機是由作業系統來決定。也就是說Thread可能在執行任何一個機器指令時,被作業系統取走CPU控制權,並交給另一個Thread。由於某些真實世界的動作是不可分割的,例如跨行轉帳X圓由A帳戶到B帳戶,轉帳前後這兩個帳戶的總金額必須相同,但以程式來實作時,卻無法用一個指令就完成,如轉帳可能要寫成下面的這一段程式碼 :
  1. if (A >= X) {  
  2.     A = A - X; // 翻譯成3個機器指令LOAD A, SUB X, STORE A  
  3.     B = B +X;  
  4. }  
如果兩個Thread同時要存取A,B兩帳戶進行轉帳,假設當Thread one執行到SUBX後被中斷,Threadtwo接手執行完成另一個轉帳要求,然後Threadone繼續執行未完成的動作,請問這兩個轉帳動作正確嗎?我們以A=1000,B=0,分別轉帳100,200圓來說明此結果 :
LOAD A // Thread 1, 現在A還是1000
SUB 100 // Thread 1
LOAD A // 假設此時Thread 1被中斷,Thread 2接手, 因為Thread 1 還沒有執行STORE A, 所以變數A還是1000
SUB 200 // Thread 2
STORE A // Thread 2, A = 800
LOAD B // Thread 2, B現在是0
ADD 200 // Thread 2
STORE B // B=200
STORE A // Thread 1拿回控制權, A = 900
LOAD B // Thread 1, B = 200
ADD 100 // Thread 1
STORE B // B = 300

你會發現執行完成後A=900,B=300,也就是說銀行平白損失了200圓。當然另外的執行順序可能造成其他不正確的結果。我們把這問題再整理一下
1. 寫程式時假設指令會循序執行
2. 某些不可分割的動作,需要以多個機器指令來完成
3. Thread執行時可能在某個機器指令被中斷
4. 兩個Thread可能執行同一段程式碼,存取同一個資料結構
5. 這樣就破壞了第1點的假設

因此在撰寫多執行緒的程式時,必須特別考慮這種狀況(又稱為race condition)。Java的解決辦法是,JVM會在每個物件上擺一把鎖(lock),然後程式設計者可以宣告執行某一段程式(通常是用來存取共同資料結構的程式碼, 又稱為Critical Section)時,必須拿到某物件的鎖才行,這個鎖同時間最多只有一個執行緒可以擁有它.
- Synchronize block demo :
  1. public class Transfer extends Thread {  
  2.     public static Object lock = new Object();  
  3.     public static int A = 1000;  
  4.     public static int B = 0;  
  5.     private int amount;  
  6.     public Transfer(int x) {  
  7.         amount = x;  
  8.     }  
  9.     public void run() {  
  10.         synchronized(lock) { // 取得lock,如果別的thread A已取得,則目前這個thread會等到thread A釋放該lock  
  11.             if (A >= amount) {  
  12.                 A = A - amount;  
  13.                 B = B + amount;  
  14.             }  
  15.         } // 離開synchronized區塊後,此thread會自動釋放lock  
  16.     }  
  17.     public static void main(String[] argv) {  
  18.         Thread t1 = new Transfer(100);  
  19.         Thread t2 = new Transfer(200);  
  20.         t1.start();  
  21.         t2.start();  
  22.     }  
  23. }  

除了synchronized(ref)的語法可以鎖定ref指到的物件外,synchronized也可以用在object method前面,表示要鎖定this物件才能執行該方法。以下是Queue結構的範例 :
  1. public class Queue {  
  2.     private Object[] data;  
  3.     private int size;  
  4.     private int head;  
  5.     private int tail;  
  6.     public Queue(int maxLen) {  
  7.         data = new Object[maxLen];  
  8.     }  
  9.     public synchronized Object deQueue() {  
  10.         Object tmp = data[head];  
  11.         data[head] = null;  
  12.         head = (head+1)%data.length;  
  13.         size--;  
  14.         return tmp;  
  15.     }  
  16.     public synchronized void enQueue(Object c) {  
  17.         data[tail++] = c;  
  18.         tail %= data.length;  
  19.         size++;  
  20.     }  
  21. }  
雖然上面的程式正確無誤,但並未考慮資源不足時該如何處理。例如Queue已經沒有資料了,卻還想拿出來;或是Queue裡已經塞滿了資料,使用者卻還要放進去?我們當然可以使用Exception Handling的機制 :
- Queue 加入 Exception on methods deQueue and enQueue :
  1. public class Queue {  
  2.     private Object[] data;  
  3.     private int size;  
  4.     private int head;  
  5.     private int tail;  
  6.     public Queue(int maxLen) {  
  7.         data = new Object[maxLen];  
  8.     }  
  9.     public synchronized Object deQueue() throws Exception {  
  10.         if (size == 0) {  
  11.             throw new Exception();  
  12.         }  
  13.         Object tmp = data[head];  
  14.         data[head] = null;  
  15.         head = (head+1)%data.length;  
  16.         size--;  
  17.         return tmp;  
  18.     }  
  19.     public synchronized void enQueue(Object c) throws Exception {  
  20.         if (size >= maxLen) {  
  21.             throw new Exception();  
  22.         }  
  23.         data[tail++] = c;  
  24.         tail %= data.length;  
  25.         size++;  
  26.     }  
  27. }  

但假設我們的執行環境是,某些Thread專門負責讀取使用者的需求,並把工作放到Queue裡面,某些Thread則專門由Queue裡抓取工作需求做進一步處理。這種架構的好處是,可以把慢速或不定速的輸入(如透過網路讀資料,連線速度可能差很多),和快速的處理分開,可使系統的反應速度更快,更節省資源。那麼以Exceptoin來處理Queue空掉或爆掉的情況並不合適,因為使用Queue的人必須處理例外狀況,並不斷的消耗CPU資源 :
- Getter/Putter 範例代碼 : 使用 Queue
  1. public class Getter extends Thread {  
  2.     Queue q;  
  3.     public Getter(Queue q) {  
  4.         this.q = q;  
  5.     }  
  6.     public void run() {  
  7.         for (;;) {  
  8.             try {  
  9.                 Object data = q.deQueue();  
  10.                 // processing  
  11.             } catch(Exception e) {  
  12.                 // if we try to sleep here, user may feel slow response  
  13.                 // if we do not sleep, CPU will be wasted  
  14.             }  
  15.         }  
  16.     }  
  17. }  
  18. public class Putter extends Thread {  
  19.     Queue q;  
  20.     public Putter(Queue q) {  
  21.         this.q = q;  
  22.     }  
  23.     public void run() {  
  24.         for (;;) {  
  25.             try {  
  26.                 Object data = null;  
  27.                 // get user request  
  28.                  q.enQueue(data);  
  29.             } catch(Exception e) {  
  30.                 // if we try to sleep here, user may feel slow response  
  31.                 // if we do not sleep, CPU will be wasted  
  32.             }  
  33.         }  
  34.     }  
  35. }  
  36. public class Main {  
  37.     public static void main(String[] argv) {  
  38.         Queue q = new Queue(10);  
  39.         Getter r1 = new Getter(q);  
  40.         Getter r2 = new Getter(q);  
  41.         Putter w1 = new Putter(q);  
  42.         Putter w2 = new Putter(q);  
  43.         r1.start();  
  44.         r2.start();  
  45.         w1.start();  
  46.         w2.start();  
  47.     }  
  48. }  

為了解決這類資源分配的問題,Java Object提供了下面三個method :
wait() : 
使呼叫此方法的Thread進入Blocking Mode,並設為等待該Object, 呼叫wait()時, 該Thread必須擁有該物件的lock。Blocking Mode下的Thread必須釋放所有手中的lock,並且無法使用CPU。

notifyAll() : 讓等待該Object的所有Thread進入Runnable Mode。
notify() : 讓等待該Object的某一個Thread進入Runnable Mode。
所謂Runnable Mode是指該Thread隨時可由作業系統分配CPU資源。Blocking Mode表示該Thread正在等待某個事件發生,作業系統不會讓這種Thread取得CPU資源。前一個Queue的範例就可以寫成 :
- 優良後的 Getter/Putter : 使用 Object 的 wait/notify/notifyAll
  1. public class Queue {  
  2.     private Object[] data;  
  3.     private int size;  
  4.     private int head;  
  5.     private int tail;  
  6.     public Queue(int maxLen) {  
  7.         data = new Object[maxLen];  
  8.     }  
  9.     public synchronized Object deQueue() {  
  10.         while (size==0) { // When executing here, Thread must have got lock and be in running mode  
  11.             // Let current Thread wait this object(to sleeping mode)  
  12.             try {  
  13.                 wait(); // to sleeping mode, and release all lock  
  14.             } catch(Exception ex) {};  
  15.         }  
  16.         Object tmp = data[head];  
  17.         data[head] = null;  
  18.         head = (head+1)%data.length;  
  19.         if (size==data.length) {  
  20.             // wake up all Threads waiting this object  
  21.             notifyAll();  
  22.         }  
  23.         size--;  
  24.         return tmp;  
  25.     } // release lock  
  26.     public synchronized void enQueue(Object c) {  
  27.         while (size==data.length) {  // When executing here, Thread must have got lock and be in running mode  
  28.             // Let current thread wait this object(to sleeping mode)  
  29.             try {  
  30.                 wait(); // to sleeping mode, and release all lock  
  31.             } catch(Exception ex) {};  
  32.         }  
  33.         data[tail++] = c;  
  34.         tail %= data.length;  
  35.         size++;  
  36.         if (size==1) {  
  37.             // wake up all Threads waiting this object  
  38.             notifyAll();  
  39.         }  
  40.     }  
  41. }  
  42.   
  43. public class ReaderWriter extends Thread {  
  44.     public static final int READER = 1;  
  45.     public static final int WRITER = 2;  
  46.     private Queue q;  
  47.     private int mode;  
  48.     public void run() {  
  49.         for (int i=0; i < 1000; i++) {  
  50.             if (mode==READER) {  
  51.                 q.deQueue();  
  52.             } else if (mode==WRITER) {  
  53.                 q.enQueue(new Integer(i));  
  54.             }  
  55.         }  
  56.     }  
  57.     public ReaderWriter(Queue q, int mode) {  
  58.         this.q = q;  
  59.         this.mode = mode;  
  60.     }  
  61.     public static void main(String[] args) {  
  62.         Queue q = new Queue(5);  
  63.         ReaderWriter r1, r2, w1, w2;  
  64.         (w1 = new ReaderWriter(q, WRITER)).start();  
  65.         (w2 = new ReaderWriter(q, WRITER)).start();  
  66.         (r1 = new ReaderWriter(q, READER)).start();  
  67.         (r2 = new ReaderWriter(q, READER)).start();  
  68.         try {  
  69.             w1.join(); // wait until w1 complete  
  70.             w2.join(); // wait until w2 complete  
  71.             r1.join(); // wait until r1 complete  
  72.             r2.join(); // wait until r2 complete  
  73.         } catch(InterruptedException epp) {  
  74.         }  
  75.     }  
  76. }  

補充說明 :
JDK6 : SwingWorker 使用說明
SwingWorker 設計用於需要在後台執行緒中運行長時間運行任務的情況,並可在完成後或者在處理過程中向 UI 提供更新。SwingWorker 的子類別必須實作 doInBackground() 方法,以執行後台計算...

[ Java設計模式 ] 多線程設計模式 : Producer-Consumer Pattern (我來做, 你來用)
這一章要學習的是 Producer-Consumer Pattern. Producer 是 “生產者” 的意思, 是指產生數據的線程. 而 Consumer 是 “消費者” 的意思, 意指使用數據的線程. 生產者必須將數據安全的交給消費者. 雖然聽起來簡單, 但當生產者與消費者在不同的線程上運行時, 兩者處理的速度差將是最大問題. 當消費者要取數據時, 生產者還沒建立數據, 或是生產者產出數據, 但是消費者還沒辦法接受數據等等. Producer-Consumer Pattern 是在生產者與消費者間加入一個 “橋樑參予者”, 這個橋樑用來緩衝兩者間的速度差...
This message was edited 14 times. Last update was at 23/02/2011 17:41:47

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!