2013年9月10日 星期二

Thread


作者: 許裕永

現在請大家回想一下上網的畫面:輸入網址以後,瀏覽器開始下載網頁,螢幕上開始顯示網頁內容。等一下。大家再注意一下:網頁內容開始顯示的時候,該網頁的 元件已經全部下載完成了嗎?答案是:不見得。也就是說:瀏覽器執行的時候是下載及顯示兩種動作同時執行。也就是說:瀏覽器這支程式,可以同時執行一個以上 的動作。所以,瀏覽器是一支多執行緒的程式。
其實大家日常使用的程式大多是多執行緒的程式,只是以前不會去注意而已。現在,我們也回頭來看看自己寫的程式,有沒有辦法同時執行兩段程式碼?當然沒有。所以目前我們所寫的程式,都只是單一執行緒,本章就是要讓大家學會撰寫多執行緒程式的基本概念及注意事項。
現在的作業系統幾乎都是多工作業系統,何謂多工?可以同時執行一個以上的程式。例如:做Word文書時,可以聽音樂,也可以上聊天室聊天,還可以………。 那也就是CPU可以同時執行一個以上的程式囉?錯。一個CPU在一個時間點只能執行一支程式,當電腦開啟一個以上程式時,是各個程式輪流執行,只是因為 CPU的運算速度快,切換的時候讓使用者不容易察覺。
執行緒的執行狀況也是一樣,當多執行緒的程式得到CPU的執行權時,會輪流交給各個執行緒執行,直到把CPU的執行權還給作業系統。而不是真所有執行緒同時執行。
1. 建立執行緒物件
撰寫多執行緒的程式,就是建構多個執行緒物件,執行緒物件啟動之後,便會執行其設定好的動作。
1-1 Thread
類別java‧lang‧Thread建構的物件就是執行緒物件,但是我們沒有辦法把程式碼寫在這個類別之中,所以我們要寫一個子類別繼承它,再把程式碼寫在run()這個方法之中。
例一:
class MyThread extends Thread{
 public void run(){
  for(int a=1;a<=20;a++){
   System.out.println(a);
  }
 }
}
示範主題:繼承Thread後,overriding run(),run()裏面撰寫的程式碼,便是此類別建構成執行緒物件而且啟動之後,要執行的動作。
 
1-2 Runnable
針對不方便繼承的類別,Java也提供了一個界面Runnable。實作這個界面,也可以overriding run()這個方法。
例一:
class MyRunnable implements Runnable{
 public void run(){
  for(int a=20;a>=1;a--){
   System.out.println(a);
  }
 }
}
示範主題:開發實作Runnable界面之類別,並overriding run()。
1-3 啟動執行緒
之前我們只是開發執行緒類別,並沒有建構執行緒物件,本小節要先教各位學會建構執行緒物件,再啟動執行緒。
例一:
public class ThreadTest{
 public static void main(String[] args){
  MyThread mt=new MyThread();
  MyRunnable mr=new MyRunnable();
  Thread t=new Thread(mr);
 }
}
class MyThread extends Thread{
 public void run(){
  for(int a=1;a<=20;a++){
   System.out.println(a);
  }
 }
}
class MyRunnable implements Runnable{
 public void run(){
  for(int a=20;a>=1;a--){
   System.out.println(a);
  }
 }
}
檔案重點:為了測試方便,可以把三個類別置於同一個檔案之中。
示範重點:1、所謂執行緒物件,指的是Thread型別的物件,MyThread是Thread的子類別,所以MyThread建構的物件便是執行緒物 件。2、Runnable界面並沒有完整的執行緒功能,所以Runnable型別的物件不算是執行緒物件。但是,可以用Runnable型別物件當參數, 呼叫Thread的建構方法,來建構執行緒物件。
 
例二:
public static void main(String[] args){
 MyThread mt=new MyThread();
 MyRunnable mr=new MyRunnable();
 Thread t=new Thread(mr);
 mt.run();
 t.run();
}
修改例一中的main。
列印結果:1~20 20~1。
示範主題:特別注意,執行緒並沒有啟動,本例仍是單一執行緒程式,它是mt‧run執行完,再執行t‧run()。
 
例三:
public static void main(String[] args){
 MyThread mt=new MyThread();
 MyRunnable mr=new MyRunnable();
 Thread t=new Thread(mr);
 mt.start();
 t.start();
}
將run()的呼叫,修改為呼叫start()。
列印結果:不一定。
示範主題:1、start()是Thread中的成員方法,用來啟動執行緒。2、多執行緒的程式,就是多個執行緒互搶執行權。
 
例四:
public static void main(String[] args){
 MyThread mt=new MyThread();
 MyRunnable mr=new MyRunnable();
 Thread t=new Thread(mr);
 mt.start();
 t.start();
 for(int a=100;a<=120;a++){
  System.out.println(a);
 }
}
檔案重點:在t‧start()底下再加上一個迴圈
列印結果:亂七八糟。
示範主題:Java也會把main建成一個執行緒,所以本程式共有三個執行緒物件在執行。
 
2. 用匿名類別方式建立執行緒
如果要執行的程式碼並不複雜,可以用匿名類別的方式建構執行緒物件。
2-1 Thread
以匿名類別撰寫Thread之子類別。
例一:
public class ThreadTest2{
 public static void main(String[] args){
  Thread t=new Thread(){
   public void run(){
    for(int a=1;a<=20;a++){
     System.out.println(a);
    }
   }
  };
  t.start();
 }
}
列印結果:1~20。
 
2-2 Runnable
以匿名類別撰寫實作Runnable界面之類別。
例一:
public class ThreadTest3{
 public static void main(String[] args){
  Thread t=new Thread(new Runnable(){
   public void run(){
    for(int a=1;a<=20;a++){
     System.out.println(a);
    }
   }
  });
  t.start();
 }
}
列印結果:1~20。
 
3. 執行緒控制
如同各位所看到的,各個執行緒不是輪流執行,而是搶著執行。誰搶到執行權就誰執行,但還沒執行多久,可能執行權又被搶走。為了有效運用執行緒的功能,Thread中定義了一些成員方法,可以讓我們用來控制執行緒的執行流程。
3-1 Priorities
每個執行緒物件,都有一個值,代表這個執行緒物件的優先等級。等級順序從低到高為1~10,若沒有設定則預設為5。要注意的是,這個值只是一個等級代號,同一個等級代號的執行緒物件可以有很多個,所以不是設為最高等級的物件就一定會最優先執行。
程式中可以用setPriority(int newPriority)來設定執行緒物件的優先等級。
例一:
public class ThreadTest{
 public static void main(String[] args){
  MyThread mt=new MyThread();
  Thread t=new Thread(new MyRunnable());
  mt.start();
  t.start();
 }
}
class MyThread extends Thread{
 public void run(){
  for(int a=1;a<=20;a++){
   if(a==2)
    setPriority(1);
   System.out.println(a);
  }
 }
}
class MyRunnable implements Runnable{
 public void run(){
  for(int a=20;a>=1;a--){
   System.out.println(a);
  }
 }
}
檔案重點:將ThreadTest檔案中的MyThread中的迴圈加上設定優先等級的敍述句。
列印結果:1 20~1 2~20。
示範主題:1、將執行緒物件的優先等級設為最低。2、執行中若把本執行緒物件的優先等級設成比目前更低的值時,執行緒物件會暫時停止執行,直到其他優先等級較高者執行完畢,才會執行。
 
例二:
將setPriority(1)中的1改為10。
列印結果;1~2020~1。
示範主題:將執行緒物件的優先等級設為最高,則此物件執行完畢之後才換其他執行緒物件執行。
Thread中提供三個類別常數成員,代表最高(10),預設(5)及最低(1)優先等級之值,請自行參閱說明文件。
 
3-2 sleep
顧名思義,此方法是讓執行緒物件暫停,而且暫停的時間可以自訂,可以用毫秒,也可以用毫微秒。要注意的是:「設定的時間,是執行緒至少暫停的時間,而不是 最多暫停的時間」。因為即使暫停時間已到,但目前正在執行的執行緒物件具有最高優先等級,那麼就必須等這個執行緒物件執行完畢,才有可能輪到暫停結束的執 行緒物件來執行。再注意一次,是有可能,不是一定,因為如果還有其他執行緒物件在等候執行,還得大家一起搶執行權,搶到才能執行。總而言之,暫停時間到 了,不見得能立刻執行。
例一:
public class ThreadTest{
 public static void main(String[] args){
  MyThread mt=new MyThread();
  Thread t=new Thread(new MyRunnable());
  mt.start();
  t.start();
 }
}
class MyThread extends Thread{
 public void run(){
  for(int a=1;a<=20;a++){
  try{
   sleep(1000);
  }catch(InterruptedException e){}
   System.out.println(a);
  }
 }
}
class MyRunnable implements Runnable{
 public void run(){
  for(int a=20;a>=1;a--){
  try{
   Thread.sleep(1000);
  }catch(InterruptedException e){}
  System.out.println(a);
  }
 }
}
列印結果:每一秒鐘兩個執行緒物件交錯列印一個數字。
示範主題:1、在Thread型別的類別中,可以直接呼叫sleep(),這個方法成員。2、在Runnable型別的類別中,並沒有sleep()這個 方法成員,但是因為sleep()是static,所以可以用類別名稱Thread呼叫。3、因為sleep()是寫在run()之中,而我們寫 run()時,其實是在overriding,所以sleep()宣告的InterruptedException物件,一定要用try catch處 理,不可以用throws宣告在run()後面。
 
3-3 yield
讓出,讓出什麼?當然是執行權。程式中如果覺得本執行緒物件可以等一下再執行,就呼叫yield()來讓出執行權。不過很明顯,它沒有指定時間,也沒有改 變優先等級,所以用這個方法讓出執行權的執行緒物件,還是會加入搶執行權的行列。也就是說:有可能又是這個物件搶到執行權而繼續執行。總之這個方法並不保 證執行緒物件會暫停。
例一:
將ThreadTest中兩個run()中的try catch整個刪掉,然後再修改MyThread的run():
public void run(){
for(int a=1;a<=20;a++){
 if(a==1)
  yield();
  System.out.println(a);
 }
}
列印結果:和沒有yield()是一樣的結果。
示範主題:yield只是讓出此次執行權,接著又跟著搶執行權,很難看出效果。
 
3-4 join
指定另一個執行緒物件執行完畢之後,本執行緒物件再執行。方法中也可以指定時間,表示指定另一個執行緒物件執行完畢或執行某一段時間之後,本執行緒物件再 執行。但其實這個方法和sleep()有一個一樣的重點,就是在指定的執行緒物件執行完畢或執行了指定了時間之後,本執行緒物件並不是立刻執行,而只是加 入搶執行緒的行列。
另外有一點要注意:被指定的執行緒物件,並不是就擁有最高優先等級而可以持續執行。這個方法只是暫停本執行緒物件的執行,而指定的執行緒物件還是得和其他執行緒物件搶執行權。
例一:
public class JoinTest{
 public static void main(String[] args){
  final Thread t1=new Thread(){
   public void run(){
    for(int a=1;a<=20;a++){
     System.out.println(a);
    }
   }
  };
  Thread t2=new Thread(){
   public void run(){
    for(int a=20;a>=1;a--){
     if(a==18){
      try{
       t1.join();
      }catch(InterruptedException e){}
     }
     System.out.println(a);
    }
   }
  };
  t1.start();
  t2.start();
 }
}
列印結果:不一定,但只要t2列印至19,就一定會等到t1列印結束才會繼續列印。
示範主題:1、在t2中執行t1‧join(),就是指定t1物件先執行。2、匿名類別屬於區域內部類別,只能存取final的區域變數,所以t1的宣告,必須加final。3、join也有宣告InterruptedException。
 
4. 執行緒同步化
多個執行緒同時執行時,如果彼此程式碼之間沒有關聯,自然沒有問題。但是如果有共同存取到某一個變數,那麼就要讓存取該變數值的程式碼和執行緒可以同步化(Synchronization)。
例一:
public class SynTest{
 int a;
 public static void main(String[] args){
  final SynTest st=new SynTest();
  Thread t1=new Thread(){
   public void run(){
    for(int b=0;b<10;b++){
     st.a++;
     System.out.println(getName() + ":" + st.a);
    }
   }
  };
  Thread t2=new Thread(){
   public void run(){
    for(int b=0;b<10;b++){
     st.a++;
     System.out.println(getName() + ":" + st.a);
    }
   }
  };
  t1.start();
  t2.start();
 }
}
檔案重點:兩個執行緒物件中,都有存取資料成員a的敍述句。
列印結果:兩個執行緒物件交錯列出1~20。
示範主題:沒有處理執行緒同步化的程式,在部份狀況下看起來好像沒問題。
 
例二:
把例一的兩個run修改如下:
public void run(){
for(int b=0;b<10;b++){
 st.a++;
 for(int c=0;c<=100000000;c++);
  System.out.println(getName() + ":" + st.a);
 }
}
檔案重點:在a運算之後,列印之前,加上一個空迴圈(請注意‘;’)來拖延執行時間(代表複雜運算),迴圈次數讀者可自行增加或減少,以符合執行電腦的效能。
列印結果:有些數字不見了,有些數字重複了。
示範主題:當執行緒對資料成員存取的方法執行完畢之前,執行緒便已中斷,等到繼續執行時,該值已被其他執行緒變更。
 
4-1 同步化方法
將資料成員宣告為private,再另外撰寫執行資料成員存取的方法,並用synchronized宣告本方法。表示本方法與執行緒物件同步,一次只讓一 個執行緒物件執行。也就是說:「本方法必須讓某一個執行緒物件執行完畢,才會讓其他執行緒物件執行。若執行期間,執行本方法的執行緒物件暫停執行,其他執 行緒物件也不可以執行本方法。」
例一:
public class SynTest{
 private int a;
 public static void main(String[] args){
  final SynTest st=new SynTest();
  Thread t1=new Thread(){
   public void run(){
    for(int b=0;b<10;b++){
     System.out.println(getName() + “:” + st.setA());
    }
   }
  };
  Thread t2=new Thread(){
   public void run(){
    for(int b=0;b<10;b++){
     System.out.println(getName() + “:” + st.setA());
    }
   }
  };
  t1.start();
  t2.start();
 }
 synchronized public int setA(){
  a++;
  for(int c=0;c<100000000;c++);
   return a;
  }
 }
檔案重點:1、將存取資料成員a的程式敍述,另外撰寫方法。2、在宣告存取資料成員的方法時,加上宣告修飾詞synchronized。3、執行緒物件不再直接存取資料成員,而是呼叫已宣告為同步化的方法。
列印結果:兩個執行緒物件交錯列印1~20。
示範主題:宣告為同步化的方法,會把運算到列印的敍述都執行完畢才讓其他執行緒物件執行。
 
4-2 同步化程式區塊
若方法中除了存取資料成員的敍述之外,還有其他許多的敍述,那麼把整個方法和執行緒同步化,就會讓其他的執行緒等待的時間明顯變長。此時,可以用同步化程式區塊的方式來撰寫。
例一:
public int setA(){
/*其他敍述*/
 synchronized(this){
  a++;
  for(int c=0;c<100000000;c++);
   return a;
 }

/*其他敍述*/
}
檔案重點:若方法中有許多敍述,可以只同步化一部份。
列印結果:兩個執行緒物件交錯列印1~20。
示範主題:同步化部份程式碼。
本例中,同步化的對象,表面上看是部份程式碼,但實際上不是。因為部份程式碼並不算是一個單位,Java執行時無法記錄。所以同步化的對象是置於 synchronized()小括號中的物件。本例中寫的是this,表示是本類別物件。在Java中每個物件都有一個標記(flag),執行緒在執行 synchronized區塊的程式碼之前,要先取得小括號中這個物件的標記,取得後開始執行synchronized區塊。而其他執行緒,就必須等待執 行中的執行緒物件,執行完synchronized區塊之後,釋放該物件的標記,才能接著取得該物件標記,再執行synchronized區塊。
也就是說synchronized的小括號中,不一定要是this,而是要與執行緒物件同步化的物件,但一定要是物件,不可以是一般變數。
請注意,在上一小節中我們學過一些Thread提供的方法,可以用來控制執行緒物件的執行,其中有一些會暫停執行緒的執行。無論執行緒是因為Thread 中的那一個方法而暫停,都不會影響同步化,也就是執行緒即使暫停,也不會釋放同步化物件的標記,其他執行緒物件自然也就持續等待。
最後,在方法不是寫得很複雜的狀況之下,比較建議用同步化方法的方式,來撰寫執行緒同步化。因為說明文件上會註明synchronized的宣告,使看說明文件的人可以了解那些方法具有執行緒安全。
 
5. 解除同步化鎖定
當方法或程式碼區塊宣告為synchronized後,除了執行緒執行完此方法或區塊,否則Thread中沒有任何方法可以解除同步化的鎖定。
其實大家再想一想,方法(區塊)的同步化和執行緒物件有關嗎?執行緒物件中的run是在控制另一個物件,是在執行另一個物件的方法,是那個物件的方法有同步化的功能,而不是執行緒物件有同步化的功能,所以解除同步化鎖定,自然和執行緒物件無關,執行緒物件是被動的。
例:
Thread t1=new Thread{
    public void run(){
        st.sales();
    }
};
執行緒物件的run()中,是執行了st‧sales()。不是t1的方法run()有同步化功能,是st的方法salse()有同步化功能,所以要解除同步化鎖定,還是得在sales()方法中撰寫。
 
要解除同步化鎖定,就必須在撰寫同步化的方法(區塊)之中呼叫wait()。當然不是無條件呼叫wait(),而是在某一個條件成立的情況之下呼叫 wait()。當wait()執行時,本方法便會解除與執行緒物件之間的同步化鎖定(釋放物件標記),然後執行緒物件便暫停執行,乖乖的在等待池中靜候通 知。
何謂等待池?執行緒物件在執行有同步化功能的方法時,因某一狀況成立,方法中執行了wait()而被解除同步化鎖定。但是這個方法只執行一半,執行緒物件 不可能先去執行其他方法,所以這樣的執行緒物件便會集中在一起,靜候通知,而集中的位置,便稱之為等待池(wait pool)。
等待誰通知?等待要它等待的物件執行notify(),則執行緒物件便可以繼續同步化執行。萬一等不到呢?那它就會痴痴的等下去,直到天荒地老,海枯石爛,這個叫死結。是高級邏輯錯誤,希望不會發生在你身上。
wait()及notiry()都是Object中的方法,也就是每個物件都有的方法。wait()有三式,可以指定時間,若時間已到卻還沒有接到通知, 也會繼續執行。而三式都會throws InterruptedException。通知有兩種:notify()及notifyAll(),後者用來通 知所有等候本物件通知的執行緒物件。
 
6. 認證重理整理
6-1建立執行緒物件
  • 财 繼承Thread後,overriding run(),run()裏面撰寫的程式碼,便是此類別建構成執行緒物件而且啟動之後,要執行的動作。
  • 财 開發實作Runnable界面之類別,也可以overriding run()。
  • 财 Runnable界面並沒有完整的執行緒功能,所以Runnable型別的物件不算是執行緒物件。但是,可以用Runnable型別物件當參數,呼叫Thread的建構方法,來建構執行緒物件。
  • 财 start()是Thread中的成員方法,用來啟動執行緒。
  • 财 多執行緒的程式,就是多個執行緒互搶執行權。
  • 财 Java也會把main建成一個執行。
6-2 以匿名類別方式建立執行緒物件
  • 财 如果要執行的程式碼並不複雜,可以用匿名類別的方式建構執行緒物件。
6-3 執行緒控制
  • 财 每個執行緒物件,都有一個值,代表這個執行緒物件的優先等級。等級順序從低到高為1~10,若沒有設定則預設為5。
  • 财 同一個等級代號的執行緒物件可以有很多個,所以不是設為最高等級的物件就一定會最優先執行。
  • 财 程式中可以用setPriority(int newPriority)來設定執行緒物件的優先等級。
  • 财 執行中若把本執行緒物件的優先等級設成比目前更低的值時,本執行緒物件會暫時停止執行,直到其他優先等級較高者執行完畢,才會執行。
  • 财 sleep()是讓執行緒物件暫停,而且暫停的時間可以自訂,可以用毫秒,也可以用毫微秒。
  • 财 sleep()設定的時間,是執行緒至少暫停的時間,而不是最多暫停的時間。
  • 财 我們寫run()時,其實是在overriding,所以sleep()宣告的InterruptedException物件,一定要用try catch處理,不可以用throws宣告在run()後面。
  • 财 yield()讓出執行權。它沒有指定時間,也沒有改變優先等級,所以用這個方法讓出執行權的執行緒物件,還是會加入搶執行權的行列。總之這個方法並不保證執行緒物件會暫停。
  • 财 join()是指定另一個執行緒物件執行完畢之後,本執行緒物件再執行。方法中也可以指定時間,表示指定另一個執行緒物件執行完畢或執行某一段時間之後,本執行緒物件再執行。
  • 财 執行join()的執行緒物件,在指定的執行緒物件執行完畢或執行了指定了時間之後,本執行緒物件並不是立刻執行,而只是加入搶執行緒的行列。
  • 财 被指定的執行緒物件,並不是就擁有最高優先等級而可以持續執行。這個方法只是暫停本執行緒物件的執行,而指定的執行緒物件還是得和其他執行緒物件搶執行權。
6-4 執行緒同步化
  • 财 用synchronized宣告方法。表示本方法與執行緒物件同步,一次只讓一個執行緒物件執行。也就是說:「本方法必須讓某一個執行緒物件執行完畢,才會讓其他執行緒物件執行。若執行期間,執行本方法的執行緒物件暫停執行,其他執行緒物件也不可以執行本方法。」
  • 财 在Java中每個物件都有一個標記(flag),執行緒在執行synchronized區塊的程式碼之前,要先取得小括號中這個物件的標記,取得後開始執 行synchronized區塊。而其他執行緒,就必須等待執行中的執行緒物件,執行完synchronized區塊之後,釋放該物件的標記,才能接著取 得該物件標記,再執行synchronized區塊。
  • 财 synchronized的小括號中,不一定要是this,而是要與執行緒物件同步化的物件,但一定要是物件,不可以是一般變數。

6-5 解除同步化鎖定
  • 财 要解除同步化鎖定,就必須在撰寫同步化的方法(區塊)之中呼叫wait()。
  • 财 當wait()執行時,本方法便會解除與執行緒物件之間的同步化鎖定(釋放物件標記),然後執行緒物件便暫停執行,乖乖的在等待池中靜候通知。
  • 财 等待池中的執行緒物件,在等待的物件執行notify()之後,執行緒物件便可以繼續同步化執行。
  • 财 wait()及notiry()都是Object中的方法,也就是每個物件都有的方法。
  • 财 wait()有三式,可以指定時間,若時間已到卻還沒有接到通知,也會繼續執行。而三式都會throws InterruptedException。
  • 财 通知有兩種:notify()及notifyAll(),後者用來通知所有等候本物件通知的執行緒物件。
 

沒有留言:

張貼留言