310 likes | 444 Views
Ch14 執行緒 (II). 物件導向系統實務. 本章大綱. 本章內容包含課本第 16 章 ( 電子書 ) : 行程與執行緒 Thread 類別 Runnable 介面 執行緒的狀態 執行緒控制 執行緒的優先權 多執行緒的同步 執行緒群組. 執行緒的狀態包括 起始狀態 ( Prepared )、 可執行狀態 ( Ready )、 正執行狀態 ( Running )、 等待狀態 ( Waiting )及 死亡狀態 ( Dead )。. 執行緒的狀態.
E N D
Ch14 執行緒(II) 物件導向系統實務
本章大綱 本章內容包含課本第16章(電子書): • 行程與執行緒 • Thread類別 • Runnable介面 • 執行緒的狀態 • 執行緒控制 • 執行緒的優先權 • 多執行緒的同步 • 執行緒群組
執行緒的狀態包括起始狀態(Prepared)、可執行狀態(Ready)、正執行狀態(Running)、等待狀態(Waiting)及死亡狀態(Dead)。執行緒的狀態包括起始狀態(Prepared)、可執行狀態(Ready)、正執行狀態(Running)、等待狀態(Waiting)及死亡狀態(Dead)。 執行緒的狀態
起始狀態(Prepared):當我們使用new關鍵字建立一個Thread物件後,未呼叫其start()方法。起始狀態(Prepared):當我們使用new關鍵字建立一個Thread物件後,未呼叫其start()方法。 可執行狀態(Ready):呼叫start()方法後,正等著排程者(thread scheduler)安排執行。 正執行狀態(Running):執行緒正在使用CPU的狀態。亦即進入執行run()方法 等待狀態(Waiting):在執行run()方法的期間,等待某事件的發生才繼續的狀態,例如呼叫sleep()方法。 死亡狀態(Dead):執行緒執行完成run()方法後即進入死亡狀態。進入死亡狀態的執行緒不能被重新啟動。 執行緒的狀態
isAlive()方法測試執行緒狀態 • 起始狀態: isAlive()為false • 可執行狀態:isAlive()為true • 正執行狀態:isAlive()為true • 等待狀態:isAlive()為true • 死亡狀態:isAlive()為flase
class Ch06_01 { public static void main(String [] args) { AliveTest at = new AliveTest(); Thread mt = new Thread( at ); System.out.println(at.state + mt.isAlive()); //列出執行緒狀態 mt.start(); while(mt.isAlive()) { try { Thread.sleep(10); } catch (Exception e) {} System.out.println(at.state + mt.isAlive()); } } } //AliveTest類別定義,實作Runnable介面 class AliveTest implements Runnable { String state = "起始狀態 "; public void run() //執行緒方法 { state = "可執行狀態 "; for(long n=0, m = 0; n<1000000; n++) //留在可執行狀態的迴圈 m += n; try { state = "等待狀態 "; Thread.sleep(150);//暫停150毫秒 } catch (Exception e) {} state = "死亡狀態 "; } } 範例1:利用isAlive()方法來測試執行緒狀態
為了避免CPU被獨佔,可以使用yield()方法讓某個執行緒進入Ready狀態,而暫時先讓出CPU。為了避免CPU被獨佔,可以使用yield()方法讓某個執行緒進入Ready狀態,而暫時先讓出CPU。 呼叫高優先權執行緒的yield()方法,可以讓低優先權執行緒有較多的機會使用到CPU,以避免低優先權執行緒處於挨餓狀態(starvation)。 執行緒控制—yield()放棄該次CPU的使用機會
執行sleep()方法:暫停執行緒一段時間。 執行suspend()方法:沒有時間限制地暫停執行緒,可呼叫resume()方法回到Ready狀態。(suspend()和resume()已被建議不要使用) 呼叫join()方法:呼叫目標執行緒的join()方法,將等到目標執行緒結束之後才會繼續。 Input/Output blocked:執行緒進行輸入輸出時,會因為輸入輸出的速度較慢而暫時進入Waiting。 呼叫同步方法時,未取得物件的lock。 執行wait()方法:放棄物件的使用權並進入等待狀態,可使用notify()方法喚醒執行緒。 執行緒控制--等待狀態
//未使用join(),主程式會不等其他執行緒,自己先完成//未使用join(),主程式會不等其他執行緒,自己先完成 class Ch06_02_01 { public static void main(String [] args) throws InterruptedException { JoinTest jt = new JoinTest(); jt.start(); System.out.println("已啟動jt~"); //jt.join(); //等待jt的結束 System.out.println("\njt已結束。"); } } //JoinTest類別定義,繼承Thread類別 class JoinTest extends Thread { public void run() //執行緒方法 { for(int i=0; i<30; i++) { try { Thread.sleep(100); } catch(InterruptedException e) {} System.out.print('O'); } } } //使用join(),主程式會等其他執行緒結束,自己才完成 class Ch06_02_02 { public static void main(String [] args) throws InterruptedException { JoinTest jt = new JoinTest(); jt.start(); System.out.println("已啟動jt~"); jt.join(); //等待jt的結束 System.out.println("\njt已結束。"); } } //JoinTest類別定義,繼承Thread類別 class JoinTest extends Thread { public void run() //執行緒方法 { for(int i=0; i<30; i++) { try { Thread.sleep(100); } catch(InterruptedException e) {} System.out.print('O'); } } } 範例2:使用join()
Java的多執行緒排程是使用簡單的「固定優先權排程」(fixed priority scheduling),這種排程會依據可執行狀態之執行緒的優先權來決定順序。 子執行緒擁有和父執行緒相同的優先權。 執行緒建立之後,就可以使用getPriority()取得優先權值,以setPriority()方法來設定優先權值。 Thread類別中定義了三個類別常數,NORM_PRIORITY是預設的優先權值,MIN_PRIORITY是最小的優先權值,MAX_PRIORITY為最大的優先權值。 執行緒的優先權
先佔先贏(preemptive scheduling):高優先權的執行緒可持續執行,直至結束或等待,除非有更高優先權的執行緒出現。Unix-like的作業系統,有可能低優先權的沒有機會執行 時間分配(Time slicing):CPU的使用被切成小段的時間,執行緒在使用過一單位的CPU時間後,就會進入Ready狀態,接著排程者會依優先權去挑選下一個執行者。優先權高的,有較大的機會執行。Windows和MacOS 執行緒的優先權 • 執行緒的排程實際上是和作業系統有關:
範例3:測試執行緒的優先權 • class EX12_5 • {public static void main(String [] args) throws InterruptedException • {PriorityTest[] p = new PriorityTest[3]; • Thread[] t = new Thread[p.length]; • for(int i=0; i<p.length; i++) • { p[i] = new PriorityTest(); • t[i] = new Thread( p[i] );} • t[0].setPriority(Thread.MAX_PRIORITY);//設定為最小優先 • t[2].setPriority(Thread.MIN_PRIORITY);//設定為最大優先 • for(int i=0; i<p.length; i++) • { System.out.print( t[i].getPriority() + "\t"); • t[i].start(); //啟動執行緒 } • System.out.println("\n--------------------"); • for(int i=0; i<10; i++) • {Thread.sleep(1000); • System.out.println(p[0].n + "\t" + p[1].n + "\t" + p[2].n); • } • System.exit(0); • }}
//PriorityTest類別定義,實作Runnable介面 class PriorityTest implements Runnable { int n; public void run() //執行緒方法 { for(int i=0; i<Integer.MAX_VALUE-1; i++) { try {Thread.sleep(1); } catch(InterruptedException e) {} n++;} }} 如果t[0]的優先權最高 如果t[0]的優先權最低 範例3:測試執行緒的優先權
多執行緒可能造成的錯誤 使用共同資源的多執行緒
範例4:使用共同資源的多執行緒 • class C06_04 • { public static void main(String [] args) • { BankAccount1 小明的帳戶 = new BankAccount1(10000); • Client1 小明 = new Client1("小明", 小明的帳戶, 2000); • Client1 媽媽 = new Client1("媽媽", 小明的帳戶, 5000); • Thread t1 = new Thread(小明); • Thread t2 = new Thread(媽媽); • System.out.println("目前的存款為" + 小明的帳戶.check()); • t1.start(); • t2.start(); • } } • class Client1 implements Runnable//模擬ATM或銀行櫃台作業 • { BankAccount1 ba; • String name; • long m; • Client1(String n, BankAccount1 ba, long m) //建構子 • { this.name = n; • this.ba = ba; • this.m = m; } • public void run() //執行緒方法 • { ba.save(name, m); //呼叫方法將錢存入 • System.out.println("存入 "+m+" 後,存款為 "+ba.check()); • } }
範例4:使用共同資源的多執行緒 • class BankAccount1 • { private long money; //存放「錢」的變數 • BankAccount1(long m) //建構子 • { money = m; //開戶時存入的錢 • } • public void save(String s, long m) //存入錢的方法 • { System.out.println(s+"開始存入"+m); • long tmp = money; //前取得目前存款 • for(long x=0; x<10000000; x++);//模擬存入所花費的時間 • money = tmp + m; //相加之後存回 • } • public long check() //取得目前存款的方法 • { return money; • } • }
使用synchronized關鍵字修飾操作共同資源的方法,可以讓該方法不會被兩個以上的執行緒同時使用,也就是在某個時間頂多只會有一個執行緒在使用該方法。使用synchronized關鍵字修飾操作共同資源的方法,可以讓該方法不會被兩個以上的執行緒同時使用,也就是在某個時間頂多只會有一個執行緒在使用該方法。 擁有synchronized修飾的方法之物件,就是一個監視器(Monitor),監視器會讓物件內的synchronized方法只讓一個執行緒使用。 若物件中的所有方法都使用sysnchronized修飾,則在某個時間點只會有一個方法被執行。因為,Monitor是物件而不是方法。 同步化方法
範例5:使用synchronized來處理同步 • class BankAccount2 • { private long money; //存放「錢」的變數 • BankAccount2(long m) //建構子 • { money = m; //開戶時存入的錢 • } • public synchronized void save(String s, long m)//存入錢的方法 • { System.out.println(s+"開始存入"+m); • long tmp = money; //前取得目前存款 • for(long x=0; x<10000000; x++);//模擬存入所花費的時間 • money = tmp + m; //相加之後存回 • } • public long check() //取得目前存款的方法 • { return money; • } • }
synchronized也可以當敘述使用 synchronized區塊中的程式敘述還未執行完畢,該物件就不會被其它執行緒所使用。 當synchronized區塊在執行時,其同步化的物件的鎖(lock)是暫時被取用的。 synchronized敘述 synchronized (物件) { //物件同步區 }
使用synchronized修飾方法和下式同義: synchronized的同步化對象其實是物件實體,因此synchronized不能用來修飾屬性、建構子或類別。 synchronized敘述 方法型別 方法名(形式參數列) { synchronized(this) { //方法內敘述 } }
範例6:以synchronized敘述來lock物件 • class Client3 implements Runnable • { BankAccount3 ba; • String name; • long m; • Client3(String n, BankAccount3 ba, long m) //建構子 • { this.name = n; • this.ba = ba; • this.m = m; • } • public void run() //執行緒方法 • { synchronized (ba) • { ba.save(name, m); //呼叫方法將錢存入 • System.out.println(“存入 ”+m+“後,存款為 "+ba.check()); • } • } • }
wait()、notify()和notifyAll()方法只能使用於synchronized修飾的方法或synchronized敘述中。wait()、notify()和notifyAll()方法只能使用於synchronized修飾的方法或synchronized敘述中。 wait()方法是讓執行緒進入等待的狀態(waiting pool),同時「放棄物件的使用權」。 wait()和sleep()兩者都可以讓執行緒進入等待狀態,不過sleep()並不會放棄物件的使用權。 notify()會隨機叫醒waiting pool中的某個執行緒,而nitifyAll()則是叫醒waiting pool中所有的執行緒。 wait()及notify()
範例7:使用wait()和notify() • class EX12_9 • { • public static void main(String [] args) • { • Dish 盤子 = new Dish(); • Putter 媽媽 = new Putter(盤子); • Taker 小強 = new Taker(盤子); • Thread t1 = new Thread(媽媽); • Thread t2 = new Thread(小強); • t1.start(); • t2.start(); • } • }
//Putter類別,實作Runnable介面 • class Putter implements Runnable • { Dish dish; • Putter(Dish dish) //建構子 • { this.dish = dish; } • public void run() //執行緒方法 • { try • { for(int i=1; i<=10; i++) • { Thread.sleep((int) (Math.random()*1000));//花費的時間 • dish.put(i); //放置餅乾 • } • } catch(InterruptedException e) {} • } } • //Taker類別,實作Runnable介面 • class Taker implements Runnable • { Dish dish; • Taker(Dish dish) //建構子 • { this.dish = dish; } • public void run() //執行緒方法 • { try • { for(int i=1; i<=10; i++) • { Thread.sleep((int) (Math.random()*1000));//花費的時間 • dish.take(); //拿走餅乾 • } • } catch(InterruptedException e) {} • } }
//Dish類別 • class Dish • { private boolean available = false; //一開始盤子內沒有餅乾 • private int cakeNum; //餅乾的編號 • public synchronized void put(int n) throws InterruptedException • { while(available) • { wait(); } • cakeNum = n; //放置新餅乾 • available = true; //盤子內有餅乾 • System.out.println("放置第 "+n+" 個餅乾。"); • notify(); //提醒,盤內已有餅乾 • } • public synchronized int take() throws InterruptedException • { while(!available) • { wait(); } • available = false; //拿走餅乾 • System.out.println("拿走第 "+cakeNum+" 個餅乾!"); • notify(); //提醒,盤內已無餅乾 • return cakeNum; • } • }
在使用wait()及notify()時,必須注意避免讓執行緒進行到「等待狀態」之後出不來,如此造成執行緒永遠都在等,而且不會結束,這種情形稱為死結(deadlock)。在使用wait()及notify()時,必須注意避免讓執行緒進行到「等待狀態」之後出不來,如此造成執行緒永遠都在等,而且不會結束,這種情形稱為死結(deadlock)。 死結算是一種邏輯上的錯誤,發生死結時,並不會丟出例外,所以要格外小心避免。 死結
練習二 使用wait()和notify撰寫一程式。 ShowN型別物件會使用Printer型別物件的printN()方法從1開始列出自然數 ShowP型別物件會使用Printer型別物件的printP()方法從2開始列出質數 自然數和質數必須交替列出
ThreadGroup建構子 如果程式中有很多執行緒,而且有些執行緒的動作是相同時(如:一起啟動),可以使用執行緒群組 執行緒群組
執行緒群組可以包含子群組和執行緒 執行緒群組
和執行緒群組相關的Thread建構子 執行緒群組
常用的ThreadGroup方法 執行緒群組