Java之線程詳解

 

 線程簡介

      隨着計算機的飛速發展,個人計算機上的操作系統也紛紛採用多任務和分時設計,將早期只有大型計算機才具有的系統特性帶到了個人計算機系統中。一般可以在同一時間內執行多個程序的操作系統都有進程的概念。一個進程就是一個執行中的程序,而每一個進程都有自己獨立的一塊內存空間、一組系統資源。在進程概念中,每一個進程的內部數據和狀態都是完全獨立的。Java程序通過流控制來執行程序流,程序中單個順序的流控制稱爲線程,多線程則指的是在單個程序中可以同時運行多個不同的線程,執行不同的任務。多線程意味着一個程序的多行語句可以看上去幾乎在同一時間內同時運行。

  線程與進程相似,是一段完成某個特定功能的代碼,是程序中單個順序的流控制;但與進程不同的是,同類的多個線程是共享一塊內存空間和一組系統資源,而線程本身的數據通常只有微處理器的寄存器數據,以及一個供程序執行時使用的堆棧。所以系統在產生一個線程,或者在各個線程之間切換時,負擔要比進程小的多,正因如此,線程被稱爲輕負荷進程(light-weight process)。一個進程中可以包含多個線程。

  一個線程是一個程序內部的順序控制流。
  1. 進程:每個進程都有獨立的代碼和數據空間(進程上下文) ,進程切換的開銷大。
   2. 線程:輕量的進程,同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程切換的開銷小。
   3. 多進程:在操作系統中,能同時運行多個任務程序。
   4. 多線程:在同一應用程序中,有多個順序流同時執行。

1.線程的概念模型

Java內在支持多線程,它的所有類都是在多線程下定義的,Java利用多線程使整個系統成爲異步系統。Java中的線程由三部分組成,如圖所示。

  1. 虛擬的CPU,封裝在java.lang.Thread類中。
  2. CPU所執行的代碼,傳遞給Thread類。
  3. CPU所處理的數據,傳遞給Thread類。

             圖一    線程
    

2. 線程體(1)

 Java的線程是通過java.lang.Thread類來實現的。當我們生成一個Thread類的對象之後,一個新的線程就產生了。

此線程實例表示Java解釋器中的真正的線程,通過它可以啓動線程、終止線程、線程掛起等,每個線程都是通過類Thread在Java的軟件包Java.lang中定義,它的構造方法爲:

   public Thread (ThreadGroup group,Runnable target,String name);

  其中,group 指明該線程所屬的線程組;target實際執行線程體的目標對象,它必須實現接口Runnable; name爲線程名。Java中的每個線程都有自己的名稱,Java提供了不同Thread類構造器,允許給線程指定名稱。如果name爲null時,則Java自動提供唯一的名稱。
當上述構造方法的某個參數爲null時,我們可得到下面的幾個構造方法:

  public Thread ();
  public Thread (Runnable target);
  public Thread (Runnable target,String name);
  public Thread (String name);
  public Thread (ThreadGroup group,Runnable target);
  public Thread (ThreadGroup group,String name);

  一個類聲明實現Runnable接口就可以充當線程體,在接口Runnable中只定義了一個方法 run():
        public void run();

  任何實現接口Runnable的對象都可以作爲一個線程的目標對象,類Thread本身也實現了接口Runnable,因此我們可以通過兩種方法實現線程體。

  (一)定義一個線程類,它繼承線程類Thread並重寫其中的方法 run(),這時在初始化這個類的實例時,目標target可爲null,表示由這個實例對來執行線程體。由於Java只支持單重繼承,用這種方法定義的類不能再繼承其它父類。

  (二)提供一個實現接口Runnable的類作爲一個線程的目標對象,在初始化一個Thread類或者Thread子類的線程對象時,把目標對象傳遞給這個線程實例,由該目標對象提供線程體 run()。這時,實現接口Runnable的類仍然可以繼承其它父類。

每個線程都是通過某個特定Thread對象的方法run( )來完成其操作的,方法run( )稱爲線程體。圖二表示了java線程的不同狀態以及狀態之間轉換所調用的方法。

  

     圖二   線程的狀態
    
  (1). 創建狀態(new Thread)
  執行下列語句時,線程就處於創建狀態:
  Thread myThread = new MyThreadClass( );
  當一個線程處於創建狀態時,它僅僅是一個空的線程對象,系統不爲它分配資源。

  (2). 可運行狀態( Runnable )
  Thread myThread = new MyThreadClass( );
  myThread.start( );
  當一個線程處於可運行狀態時,系統爲這個線程分配了它需的系統資源,安排其運行並調用線程運行方法,這樣就使得該線程處於可運行( Runnable )狀態。需要注意的是這一狀態並不是運行中狀態(Running ),因爲線程也許實際上並未真正運行。由於很多計算機都是單處理器的,所以要在同一時刻運行所有的處於可運行狀態的線程是不可能的,Java的運行系統必須實現調度來保證這些線程共享處理器。
  
  (3). 不可運行狀態(Not Runnable)
  進入不可運行狀態的原因有如下幾條:
  1) 調用了sleep()方法;
  2) 調用了suspend()方法;
  3) 爲等候一個條件變量,線程調用wait()方法;
  4) 輸入輸出流中發生線程阻塞;
  不可運行狀態也稱爲阻塞狀態(Blocked)。因爲某種原因(輸入/輸出、等待消息或其它阻塞情況),系統不能執行線程的狀態。這時即使處理器空閒,也不能執行該線程。

  (4). 死亡狀態(Dead)
  線程的終止一般可通過兩種方法實現:自然撤消(線程執行完)或是被停止(調用stop()方法)。目前不推薦通過調用stop()來終止線程的執行,而是讓線程執行完。

線程體(2)

 ◇線程體的構造

  任何實現接口Runnable的對象都可以作爲一個線程的目標對象,上面已講過構造線程體有兩種方法,下面通過實例來說明如何構造線程體的。

例1 通過繼承類Thread構造線程體
  class SimpleThread extends Thread {
  public SimpleThread(String str) {
   super(str); //調用其父類的構造方法
  }
  public void run() { //重寫run方法
   for (int i = 0; i < 10; i++) {
    System.out.println(i + " " + getName());
             //打印次數和線程的名字
    try {
      sleep((int)(Math.random() * 1000));
             //線程睡眠,把控制權交出去
    } catch (InterruptedException e) {}
  }

     System.out.println("DONE! " + getName());
             //線程執行結束
    }
  }
  public class TwoThreadsTest {
   public static void main (String args[]) {
    new SimpleThread("First").start();
             //第一個線程的名字爲First
    new SimpleThread("Second").start();
             //第二個線程的名字爲Second
}
}

   運行結果:
    0 First
    0 Second
    1 Second
    1 First
    2 First
    2 Second
    3 Second
    3 First
    4 First
    4 Second
    5 First
    5 Second
    6 Second
    6 First
    7 First
    7 Second
    8 Second
    9 Second
    8 First
    DONE! Second
    9 First
    DONE! First

  仔細分析一下運行結果,會發現兩個線程是交錯運行的,感覺就象是兩個線程在同時運行。但是實際上一臺計算機通常就只有一個CPU,在某個時刻只能是隻有一個線程在運行,而java語言在設計時就充分考慮到線程的併發調度執行。對於程序員來說,在編程時要注意給每個線程執行的時間和機會,主要是通過讓線程睡眠的辦法(調用sleep()方法)來讓當前線程暫停執行,然後由其它線程來爭奪執行的機會。如果上面的程序中沒有用到sleep()方法,則就是第一個線程先執行完畢,然後第二個線程再執行完畢。所以用活sleep()方法是學習線程的一個關鍵。

例2 通過接口構造線程體
   public class Clock extends java.applet.Applet implements Runnable {//實現接口
      Thread clockThread;
       public void start() {
         //該方法是Applet的方法,不是線程的方法
      if (clockThread == null) {
          clockThread = new Thread(this, "Clock");
         /*線程體是Clock對象本身,線程名字爲"Clock"*/
         clockThread.start(); //啓動線程
         }
      }

      public void run() { //run()方法中是線程執行的內容
         while (clockThread != null) {
         repaint(); //刷新顯示畫面
         try {
           clockThread.sleep(1000);
           //睡眠1秒,即每隔1秒執行一次
          } catch (InterruptedException e){}
         }
      }

      public void paint(Graphics g) {
          Date now = new Date(); //獲得當前的時間對象
          g.drawString(now.getHours() + ":" + now.getMinutes()+ ":" +now.getSeconds(), 5, 10);//顯示當前時間
      }

      public void stop() {
        //該方法是Applet的方法,不是線程的方法
          clockThread.stop();
          clockThread = null;
      }
   }

  上面這個例子是通過每隔1秒種就執行線程的刷新畫面功能,顯示當前的時間;看起來的效果就是一個時鐘,每隔1秒就變化一次。由於採用的是實現接口Runnable的方式,所以該類Clock還繼承了Applet, Clock就可以Applet的方式運行。

  構造線程體的兩種方法的比較:

  1. 使用Runnable接口
   1) 可以將CPU,代碼和數據分開,形成清晰的模型;
   2) 還可以從其他類繼承;
   3) 保持程序風格的一致性。

  2. 直接繼承Thread類
   1) 不能再從其他類繼承;
   2) 編寫簡單,可以直接操縱線程,無需使用Thread.currentThread()。

3 線程的調度

Java提供一個線程調度器來監控程序中啓動後進入就緒狀態的所有線程。線程調度器按照線程的優先級決定應調度哪些線程來執行。
  線程調度器按線程的優先級高低選擇高優先級線程(進入運行中狀態)執行,同時線程調度是搶先式調度,即如果在當前線程執行過程中,一個更高優先級的線程進入可運行狀態,則這個線程立即被調度執行。
搶先式調度又分爲:時間片方式和獨佔方式。在時間片方式下,當前活動線程執行完當前時間片後,如果有其他處於就緒狀態的相同優先級的線程,系統會將執行權交給其他就緒態的同優先級線程;當前活動線程轉入等待執行隊列,等待下一個時間片的調度。

  在獨佔方式下,當前活動線程一旦獲得執行權,將一直執行下去,直到執行完畢或由於某種原因主動放棄CPU,或者是有一高優先級的線程處於就緒狀態。

  下面幾種情況下,當前線程會放棄CPU:

  1. 線程調用了yield() 或sleep() 方法主動放棄;

   2. 由於當前線程進行I/O 訪問,外存讀寫,等待用戶輸入等操作,導致線程阻塞;或者是爲等候一個條件變量,以及線程調用wait()方法;

   3. 搶先式系統下,由高優先級的線程參與調度;時間片方式下,當前時間片用完,由同優先級的線程參與調度。

  線程的優先級

  線程的優先級用數字來表示,範圍從1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORITY。一個線程的缺省優先級是5,即Thread.NORM_PRIORITY。下述方法可以對優先級進行操作:

  int getPriority(); //得到線程的優先級
  void setPriority(int newPriority);
   //當線程被創建後,可通過此方法改變線程的優先級

例6.3中生成三個不同線程,其中一個線程在最低優先級下運行,而另兩個線程在最高優先級下運行。

例6.3
  class ThreadTest{
    public static void main( String args [] ) {
      Thread t1 = new MyThread("T1");
      t1.setPriority( Thread.MIN_PRIORITY ); //設置優先級爲最小
      t1.start( );
      Thread t2 = new MyThread("T2");
      t2.setPriority( Thread.MAX_PRIORITY ); //設置優先級爲最大
      t2.start( );
      Thread t3 = new MyThread("T3");
      t3.setPriority( Thread.MAX_PRIORITY ); //設置優先級爲最大
      t3.start( );
    }
        }

   class MyThread extends Thread {
     String message;
     MyThread ( String message ) {
        this.message = message;
     }
     public void run() {
       for ( int i=0; i<3; i++ )
        System.out.println( message+" "+getPriority() );
                         //獲得線程的優先級
     }
        }

  運行結果:
       T2 10
       T2 10
       T2 10
       T3 10
       T3 10
       T3 10
       T1 1
       T1 1
       T1 1

  注意:並不是在所有系統中運行Java程序時都採用時間片策略調度線程,所以一個線程在空閒時應該主動放棄CPU,以使其他同優先級和低優先級的線程得到執行。





4基本的線程控制
(1).終止線程


  線程終止後,其生命週期結束了,即進入死亡態,終止後的線程不能再被調度執行,以下幾種情況,線程進入終止狀態:
  1) 線程執行完其run()方法後,會自然終止。
  2) 通過調用線程的實例方法stop()來終止線程。

 (2). 測試線程狀態


  可以通過Thread 中的isAlive() 方法來獲取線程是否處於活動狀態;線程由start() 方法啓動後,直到其被終止之間的任何時刻,都處於'Alive'狀態。

  (3). 線程的暫停和恢復


  有幾種方法可以暫停一個線程的執行,在適當的時候再恢復其執行。
  1) sleep() 方法
  當前線程睡眠(停止執行)若干毫秒,線程由運行中狀態進入不可運行狀態,停止執行時間到後線程進入可運行狀態。

  2) suspend()和resume()方法
  線程的暫停和恢復,通過調用線程的suspend()方法使線程暫時由可運行態切換到不可運行態,若此線程想再回到可運行態,必須由其他線程調用resume()方法來實現。
  注:從JDK1.2開始就不再使用suspend()和resume()。

  3) join()
  當前線程等待調用該方法的線程結束後, 再恢復執行.
  TimerThread tt=new TimerThread(100);
  tt.start();
  …
  public void timeout(){
  tt.join();// 當前線程等待線程tt 執行完後再繼續往下執行
   … }

多線程的互斥與同步

臨界資源問題
  前面所提到的線程都是獨立的,而且異步執行,也就是說每個線程都包含了運行時所需要的數據或方法,而不需要外部的資源或方法,也不必關心其它線程的狀態或行爲。但是經常有一些同時運行的線程需要共享數據,此時就需考慮其他線程的狀態和行爲,否則就不能保證程序的運行結果的正確性。例6.4說明了此問題。

例4
   class stack{
   int idx=0; //堆棧指針的初始值爲0
   char[ ] data = new char[6]; //堆棧有6個字符的空間

   public void push(char c){ //壓棧操作
    data[idx] = c; //數據入棧
    idx + +; //指針向上移動一位
   }

     public char pop(){ //出棧操作
       idx - -; //指針向下移動一位
       return data[idx]; //數據出棧
     }
   }

  --觀看動畫--

  兩個線程A和B在同時使用Stack的同一個實例對象,A正在往堆棧裏push一個數據,B則要從堆棧中pop一個數據。如果由於線程A和B在對Stack對象的操作上的不完整性,會導致操作的失敗,具體過程如下所示:

  1) 操作之前
      data = | p | q | | | | | idx=2


  2) A執行push中的第一個語句,將r推入堆棧;
     data = | p | q | r | | | | idx=2

  3) A還未執行idx++語句,A的執行被B中斷,B執行pop方法,返回q:
     data = | p | q | r | | | | idx=1

  4〕A繼續執行push的第二個語句:
     data = | p | q | r | | , | | idx=2
  
  最後的結果相當於r沒有入棧。產生這種問題的原因在於對共享數據訪問的操作的不完整性。

 互斥鎖

爲解決操作的不完整性問題,在Java 語言中,引入了對象互斥鎖的概念,來保證共享數據操作的完整性。每個對象都對應於一個可稱爲" 互斥鎖" 的標記,這個標記用來保證在任一時刻,只能有一個線程訪問該對象。 關鍵字synchronized 來與對象的互斥鎖聯繫。當某個對象用synchronized 修飾時,表明該對象在任一時刻只能由一個線程訪問。

    public void push(char c){
    synchronized(this){ //this表示Stack的當前對象
       data[idx]=c;
       idx++;
    }
    }
    public char pop(){
       synchronized(this){ //this表示Stack的當前對象
       idx--;
       return data[idx];
       }
    }
synchronized 除了象上面講的放在對象前面限制一段代碼的執行外,還可以放在方法聲明中,表示整個方法爲同步方法。
  public synchronized void push(char c){
  …
    }

如果synchronized用在類聲明中,則表明該類中的所有方法都是synchronized的。

多線程的同步

  本節將討論如何控制互相交互的線程之間的運行進度,即多線程之間的同步問題,下面我們將通過多線程同步的模型: 生產者-消費者問題來說明怎樣實現多線程的同步。
 我們把系統中使用某類資源的線程稱爲消費者,產生或釋放同類資源的線程稱爲生產者
 在下面的Java的應用程序中,生產者線程向文件中寫數據,消費者從文件中讀數據,這樣,在這個程序中同時運行的兩個線程共享同一個文件資源。通過這個例子我們來了解怎樣使它們同步。

例5
   class SyncStack{ //同步堆棧類
   private int index = 0; //堆棧指針初始值爲0
   private char []buffer = new char[6]; //堆棧有6個字符的空間

   public synchronized void push(char c){ //加上互斥鎖
     while(index = = buffer.length){ //堆棧已滿,不能壓棧
     try{
        this.wait(); //等待,直到有數據出棧
       }catch(InterruptedException e){}
       }

   this.notify(); //通知其它線程把數據出棧
   buffer[index] = c; //數據入棧
   index++; //指針向上移動
   }

   public synchronized char pop(){ //加上互斥鎖
       while(index ==0){ //堆棧無數據,不能出棧
        try{
           this.wait(); //等待其它線程把數據入棧
        }catch(InterruptedException e){}
          }

       this.notify(); //通知其它線程入棧
       index- -; //指針向下移動
       return buffer[index]; //數據出棧
    }
       }

    class Producer implements Runnable{ //生產者類
       SyncStack theStack;
        //生產者類生成的字母都保存到同步堆棧中

       public Producer(SyncStack s){
          theStack = s;
       }

       public void run(){
          char c;
          for(int i=0; i<20; i++){
            c =(char)(Math.random()*26+'A');
                          //隨機產生20個字符
            theStack.push(c); //把字符入棧
            System.out.println("Produced: "+c); //打印字符
            try{
            Thread.sleep((int)(Math.random()*1000));
                     /*每產生一個字符線程就睡眠*/
            }catch(InterruptedException e){}
          }
       }
     }

     class Consumer implements Runnable{ //消費者類
         SyncStack theStack;
                  //消費者類獲得的字符都來自同步堆棧

         public Consumer(SyncStack s){
             theStack = s;
         }

         public void run(){
             char c;
             for(int i=0;i<20;i++){
               c = theStack.pop(); //從堆棧中讀取字符
             System.out.println("Consumed: "+c);
                             //打印字符
             try{
             Thread.sleep((int)(Math.random()*1000));
                    /*每讀取一個字符線程就睡眠*/
             }catch(InterruptedException e){}
         }
       }
     }

     public class SyncTest{
       public static void main(String args[]){
         SyncStack stack = new SyncStack();
   //下面的消費者類對象和生產者類對象所操作的是同一個同步堆棧對象
         Runnable source=new Producer(stack);
         Runnable sink = new Consumer(stack);
         Thread t1 = new Thread(source); //線程實例化
         Thread t2 = new Thread(sink); //線程實例化
         t1.start(); //線程啓動
         t2.start(); //線程啓動
       }
     }

  類Producer是生產者模型,其中的 run()方法中定義了生產者線程所做的操作,循環調用push()方法,將生產的20個字母送入堆棧中,每次執行完push操作後,調用sleep()方法睡眠一段隨機時間,以給其他線程執行的機會。類Consumer是消費者模型,循環調用pop()方法,從堆棧中取出一個數據,一共取20次,每次執行完pop操作後,調用sleep()方法睡眠一段隨機時間,以給其他線程執行的機會。

  程序執行結果
        Produced:V
        Consumed:V
        Produced:E
        Consumed:E
        Produced:P
        Produced:L
        ...
        Consumed:L
        Consumed:P

  在上述的例子中,通過運用wait()和notify()方法來實現線程的同步,在同步中還會用到notifyAll()方法,一般來說,每個共享對象的互斥鎖存在兩個隊列,一個是鎖等待隊列,另一個是鎖申請隊列,鎖申請隊列中的第一個線程可以對該共享對象進行操作,而鎖等待隊列中的線程在某些情況下將移入到鎖申請隊列。下面比較一下wait()、notify()和notifyAll()方法:

  (1) wait,nofity,notifyAll必須在已經持有鎖的情況下執行,所以它們只能出現在synchronized作用的範圍內,也就是出現在用       synchronized修飾的方法或類中。

  (2) wait的作用:釋放已持有的鎖,進入等待隊列.
  
  (3) notify的作用:喚醒wait隊列中的第一個線程並把它移入鎖申請隊列.

  (4) notifyAll的作用:喚醒wait隊列中的所有的線程並把它們移入鎖申請隊列.

  注意:
  1) suspend()和resume()
    在JDK1.2中不再使用suspend()和resume(),其相應功能由wait()和notify()來實現。

  2) stop()
    在JDK1.2中不再使用stop(),而是通過標誌位來使程序正常執行完畢。例6.6就是一個典型的例子。

例6
   public class Xyz implements Runnable {
      private boolean timeToQuit=false; //標誌位初始值爲假
      public void run() {
         while(!timeToQuit) {//只要標誌位爲假,線程繼續運行
             …
         }
      }

   public void stopRunning() {
         timeToQuit=true;} //標誌位設爲真,表示程序正常結束
      }
   public class ControlThread {
      private Runnable r=new Xyz();
      private Thread t=new Thread(r);
      public void startThread() {
         t.start();
      }
      public void stopThread() {
         r.stopRunning(); }
               //通過調用stopRunning方法來終止線程運行
      }



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章