現在 CPU 速度越來越快啦! 若是要更妥善利用他,希望他可以同時間多做一些事情,程式要怎麼寫呢?? 答案就是「執行緒(Thread)」。
開出新的 thread 的 sample:
Thread thread = new Thread(new Runnable() { public void run() { while(true) System.out.print("T"); } }); //必須使用 setDaemon() method 將 thread 設定為 daemon //否則 main thread 結束後,此 thread 還是會繼續執行 thread.setDaemon(true); thread.start();
執行緒生命週期
thread 的狀態有四種,分別是:
- New Thread (建立執行緒)
- Runnable
- Not Runnable
- Dead
當 new 一個 thread 物件並執行 start() method 後,會進入 Runnable 狀態,而 JVM 中會有一個 scheduler 專門負責處理所有狀態為 Runnable 的 thread 排程,因此即使狀態是 Runnable 的 thread,也必須要被排入執行才會真的執行 run() method 中的程式。而 thread 之間工作切換的速度很快,因此看來就像在同時執行一般。
而 thread 執行時也有其優先順序,可以利用 setPriority() method 去進行調整(1~10),數值越大優先權越高。
其中比較需要注意的是 yield() method,這是在不支援 Timeslicing 的 OS 上撰寫 multi-thread 程式才需要用到的 method,可讓目前執行中的 thread 暫停,去執行另外一個 thread 的工作;而當 thread 使用 yield() 讓出執行權時,他會再度回到 Runnable 的狀態等待 scheduler 的排程。
接著,如上圖所示,有幾個狀況會讓 thread 進入 Not Runnable 的狀態:
會從 Runnable 轉換為 Not Runnable 的情形大多是發生在 thread 等待使用者的文字輸入、傾聽網路連線…..等情況時,而若是發生以下情形,則 thead 會再度回到 Runnable 的狀態:
- 完成 I/O 的動作
- 呼叫 interrupt() method()
- 呼叫 notify()、notifyAll() …. 等 method
- 取得物件鎖定
最後,當工作完成或發生例外,而離開了 run() method,則進入 Dead 狀態,可透過 isAlive() method 來測試 thread 是否還存活。
範例來說明:
public class InterruptDemo { public static void main(String args[]) { Thread thread = new Thread(new Runnable() { public void run() { try { //使用 sleep(),進入 Not Runnable 狀態 Thread.sleep(99999); } catch(InterruptedException e) { System.out.println("I'm interrupted !!"); } } }); thread.start(); //使用 interrupt(),回到 Runnable 狀態 //但會丟出 InterruptedException 例外 thread.interrupt(); } }
若要暫停 thread 的執行,但時間不確定是多久,使用 sleep() 就不是一個好方式了,此時可以用 wait() 讓 thread 進入 Not Runnable 的狀態,並透過別的 thread 使用 notify() or notifyAll() 讓 thread 的狀態回到 Runnable。
執行緒的加入
若 Thread A 正在執行,臨時需要加入 Thread B,並優先處理,此時可以用 join() method 來處理此種情況。以下用個範例程式來說明:
public class ThreadA { public static void main(String args[]) { System.out.println("Thread A 執行"); Thread threadB = new Thread(new Runnable() { public void run() { try { System.out.println("Thread B 開始"); for(int i = 0 ; i < 5 ; i++) { Thread.sleep(1000); System.out.println("Thread B 執行......."); } System.out.println("Thread B 即將結束"); } catch(InterruptedException e) { e.printStackTrace(); } } }); threadB.start(); try { threadB.join(); //使用 join() } catch(InterruptedException e) { e.printStackTrace(); } //threadB 透過 join() 加入,因此必須等 threadB 執行完後才會執行此行 //若沒用 join(),很快就會執行到這邊 System.out.println("Thread A 執行"); } }
執行緒的停止
在 J2SE 6.0 API 中,Thread class 中的 stop()、suspend()、resume() ….等 method,分別都被註記為「Deprecated」,因此表示使用者若需要這些功能,就必須自己實作囉!
以 stop 來說,若需要讓 thread 停止,就必須要想辦法讓 thread 完成 run() 中的流程,以下用個程式作範例:
public class SomeThread implements Runnable { public static void main(String args[]) { Thread thread = new Thread(new SomeThread()); thread.start(); //透過 interrupt() 丟出 InterruptedException 例外 //讓 run() 順利結束 thread.interrupt(); } public void run() { System.out.println("sleep......至 block 狀態"); try { Thread.sleep(9999); } catch(InterruptedException e) { System.out.println("I am interrupted"); } }
ThreadGroup
每一個 thread 都隸屬於某一個 ThreadGroup,若產生的 thread 沒有指定 ThreadGroup,則此 thread 則屬於產生它的 thread (一般來說是 main thread)。而必須注意的是,當 thread 歸入某個 ThreadGroup 後,就無法再更換到其他 ThreadGroup 了!
而透過 java.lang.ThreadGroup,可以管理整個 group 中的 thread,例如 interrupt 群組中所有的 thread,或是設定優先權….等等,詳細的操作方式可以參考官方文件。
此外,若在 ThreadGroup 中有 thread 發生了例外,可透過實作 Thread.UncaughtExceptionHandler 的方式來進行處理,以下用一段簡單的程式碼來介紹:
ThreadExceptionHandler.java
public class ThreadExceptionHandler implements Thread.UncaughtExceptionHandler { public void uncaughtException(Thread t, Throwable e) { System.out.println(t.getName() + " : " + e.getMessage()); } }
ThreadGroupDemo.java
public class ThreadGroupDemo { public static void main(String args[]) { ThreadExceptionHandler handler = new ThreadExceptionHandler(); ThreadGroup group1 = new ThreadGroup("group1"); //指定 thread 屬於 group1 Thread thread1 = new Thread(group1, new Runnable() { public void run() { //丟出例外由指定的 Handler 處理 //但也同時結束了 thread throw new RuntimeException("測試例外"); } }); thread1.setUncaughtExceptionHandler(handler); //設定例外處理的 Hanlder thread1.start(); } }
執行緒同步(Synchronized)
當有一個物件,同時可能有多個 thread 進行存取時,就會可能有資料不同步的情況發生,此時在寫程式時就必須考慮到這個問題並加以解決,以下用一個範例程式來說明:
PersonalInfo.java
public class PersonalInfo { private String name; private String id; private int count; public PersonalInfo() { name = "nobody"; id = "N/A"; } public synchronized void setNameAndID(String name, String id) { this.name = name; this.id = id; if(!checkNameAndIDEqual()) System.out.println(count + ") illegal name or ID....."); count++; } private boolean checkNameAndIDEqual() { return (name.charAt(0) == id.charAt(0)) ? true : false; } }
PersonalInfoTest.java
public class PersonalInfoTest { public static void main(String args[]) { final PersonalInfo person = new PersonalInfo(); Thread t1 = new Thread(new Runnable() { public void run() { while(true) person.setNameAndID("Tzeng Chien Ming", "TCM"); } }); Thread t2 = new Thread(new Runnable() { public void run() { while(true) person.setNameAndID("Leon Tzeng", "LT"); } }); //這邊多跑幾次就會出錯了 //但若在 method 前加入「synchronized」關鍵字就可以將物件鎖定 System.out.println("開始測試...."); t1.start(); t2.start(); } }
在這邊,所使用的關鍵字就是「synchronized」,透過此關鍵字,就可以將 PersonalInfo 物件鎖定,確保不會有同時有多個 thread 執行使用關鍵字 synchronized 設定過的 setNameAndID() method。
此外,synchronized 關鍵字不僅可以用於於 method,也可以用在任何程式片段中。