天天看點

JAVA教程 第六講 Java的線程和Java Applet

 

6.1.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

       T3 10

       T1 1

  

注意:

并不是在所有系統中運作Java程式時都采用時間片政策排程線程,是以一個線程在空閑時應該主動放棄CPU,以使其他同優先級和低優先級的線程得到執行。

6.1.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.2多線程的互斥與同步

臨界資源問題

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

例6.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沒有入棧。産生這種問題的原因在于對共享資料通路的操作的不完整性。

6.2.1 互斥鎖

  為解決操作的不完整性問題,在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的。

6.2.2多線程的同步

  本節将讨論如何控制互互相動的線程之間的運作進度,即多線程之間的同步問題,下面我們将通過多線程同步的模型: 生産者-消費者問題來說明怎樣實作多線程的同步。

  我們把系統中使用某類資源的線程稱為消費者,産生或釋放同類資源的線程稱為生産者。

在下面的Java的應用程式中,生産者線程向檔案中寫資料,消費者從檔案中讀資料,這樣,在這個程式中同時運作的兩個線程共享同一個檔案資源。通過這個例子我們來了解怎樣使它們同步。

 例6.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.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方法來終止線程運作

6.3 Java Applet

  前面的章節我們闡述了Application的應用,這一講我們将介紹java的另一類應用java Applet,即java小應用程式。

  在Java問世的頭幾年裡,之是以如此熱門,其根本原因還是在于Java具有"讓Internet動起來"的能力。具體地說,就是Java能建立一種特殊類型的程式(通常稱作"小應用程式"或者Applet),具備Java能力的Web浏覽器可從網上下載下傳這種程式,然後運作。

  目前,幾乎所有浏覽器均支援動态HTML(DHTML)和腳本編制(支援XML的浏覽器也有很多),是以比起Java剛剛問世的時候,浏覽器能夠做的事情要多得多。但盡管如此,由于小應用程式是用一種全功能的程式設計語言編制的,是以同HTML、XML和腳本語言的任何一種可能的組合相比,它仍然具有應用前景!

6.3.1 Applet 介紹(1)

  Applet就是使用Java語言編寫的一段代碼,它可以在浏覽器環境中運作。它與Application的差別主要在于其執行方式的不同。application 是從其中的main() 方法開始運作的,而Applet 是在浏覽器中運作的,必須建立一個HTML 檔案,通過編寫HTML 語言代碼告訴浏覽器載入何種Applet 以及如何運作。

例6.7 HelloWorld.java 源程式:

  import java.awt.Graphics; //引入圖形類Graphics

  import java.applet.Applet; //引入Applet類

  public class HelloWorld extends Applet {

      String hw_text ;

      public void init () { //init()方法是Applet首先執行的方法

      hw_text = "Hello World";

      public void paint(Graphics g) {

      g.drawString (hw_text , 25, 25) ;

         //在坐标為(25,25)的地方顯示字元串hw_text

  }

  Applet程式編寫完後,首先要用java編譯器編譯成為位元組碼檔案,然後編寫相應的HTML檔案才能夠正常執行,例如為運作上面的Applet程式所編寫的HTML檔案HelloWorld.html如下:

  <HTML>

  <APPLET CODE="HelloWorld.class" WIDTH=200 HEIGHT=100

  </APPLET>

  </HTML>

6.3.1 Applet 介紹(2)

2.Applet的 安全性

  "沙箱"機制:Java虛拟機為Applet提供能夠良好運作的沙箱,一旦它們試圖離開沙箱則會被禁止。

  由于小應用程式是通過網絡傳遞的,這就不可避免地使人想到會發生安全問題。例如有人編寫惡意程式通過小應用程式讀取使用者密碼并散播到網絡上,這将會是一件非常可怕的事情。是以,必須對小應用程式進行限制。

  浏覽器禁止Applet執行下列操作:

  (1)在運作時調用其它程式。

  (2)檔案讀寫操作。

  (3)裝載動态連接配接庫和調用任何本地方法。

  (4)試圖打開一個socket進行網絡通信,但是所連接配接的主機并不是提供Applet的主機。

 ◇ Applet的類層次:

JAVA教程 第六講 Java的線程和Java Applet

 ◇ Applet的生命周期

  小應用程式的生命周期相對于Application而言較為複雜。在其生命周期中涉及到Applet類的四個方法(也被JApplet類繼承):init()、start()、stop()和destroy()。下面首先用圖來表示一個小應用程式的生命周期,然後再簡要描述這四個方法。   看圖

  Applet的生命周期中有四個狀态:初始态、運作态、停止态和消亡态。當程式執行完init()方法以後,Applet程式就進入了初始态;然後馬上執行start()方法,Applet程式進入運作态;當Applet程式所在的浏覽器圖示化或者是轉入其它頁面時,該Applet程式馬上執行stop()方法,Applet程式進入停止态;在停止态中,如果浏覽器又重新裝載該Applet程式所在的頁面,或者是浏覽器從圖示中複原,則Applet程式馬上調用start()方法,進入運作态;當然,在停止态時,如果浏覽器關閉,則Applet程式調用destroy()方法,進入消亡态。

 ◇ Applet的主要方法:

1. init( )

  建立Applet時執行,隻執行一次

  當小應用程式第一次被支援Java的浏覽器加載時,便執行該方法。在小應用程式的生命周期中,隻執行一次該方法,是以可以在其中進行一些隻執行一次的初始化操作,如處理由浏覽器傳遞進來的參數、添加使用者接口元件、加載圖像和聲音檔案等。

小應用程式有預設的構造方法,但它習慣于在init()方法中執行所有的初始化,而不是在預設的構造方法内。

2.start( )

  多次執行,當浏覽器從圖示恢複成視窗,或者是傳回該首頁時執行。

  系統在調用完init()方法之後,将自動調用start()方法。而且每當浏覽器從圖示恢複為視窗時,或者使用者離開包含該小應用程式的首頁後又再傳回時,系統都會再執行一遍start()方法。start()方法在小應用程式的生命周期中被調用多次,以啟動小應用程式的執行,這一點與init()方法不同。該方法是小應用程式的主體,在其中可以執行一些需要重複執行的任務或者重新激活一個線程,例如開始動畫或播放聲音等。

3.stop( )

  多次執行,當浏覽器變成圖示時,或者是離開首頁時執行,主要功能是停止一些耗用系統資源的工作,。

  與start()相反,當使用者離開小應用程式所在頁面或浏覽器變成圖示時,會自動調用stop()方法。是以,該方法在生命周期中也被多次調用。這樣使得可以在使用者并不注意小應用程式的時候,停止一些耗用系統資源的工作(如中斷一個線程),以免影響系統的運作速度,且并不需要去人為地去調用該方法。如果你的小應用程式中不包含動畫、聲音等程式,通常也不必重載該方法。

4.destroy( )

  用來釋放資源,在stop( )之後執行

  浏覽器正常關閉時,Java自動調用這個方法。destroy()方法用于回收任何一個與系統無關的記憶體資源。當然,如果這個小應用程式仍然處于活動狀态,Java會在調用destroy()之前,調用stop()方法。

  下面的例子使用了小應用程式生命周期中的這幾個方法。

 例6.8

  import java.awt.Graphics;

  import java.applet.Applet;

  public class HWloop extends Applet {

     AudioClip sound; //聲音片斷對象

     public void init( ){

        sound=getAudioClip("audio/hello.au"); //獲得聲音片斷

     public void paint(Graphics g) {

        g.drawString("hello Audio",25,25); //顯示字元串

  public void start( )

     {

        sound.loop( ); //聲音片斷開始播放

  public void stop( )

        sound.stop( ); //聲音片斷停止

  }

  在本例子中,Applet開始執行後就不停的播放聲音,如果浏覽器圖示化或者是轉到其它頁面,則聲音停止播放;如果浏覽器恢複正常或者是從其它頁面跳回,則程式繼續播放聲音。

  由于Applet程式要嵌入到HTML檔案中才能夠正常執行,下面介紹與Applet程式有關的HTML檔案标記。

  <applet>

  [archive=archiveList] //同一個codebase中需預先下載下傳的檔案

  code=appletFile.class //applet的名稱

  width=pixels height=pixels //applet的初始顯示空間

  [codebase=codebaseURL] //代碼的位址

  [alt=alternateText] //如果浏覽器不支援applet時,顯示的替代文本

  [name=appletInstanceName]

           //給該applet取名,用于同頁面applet之間的通信

  [align=alignment] //對齊方式,如left,right,top,baseline...

  [vspace=pixels] [hspace=pixels] //預留的邊緣空白

  >

  [<param name=appletAttribute1 value=value>]

  [<param name=appletAttribute2 value=value>]

  …… //參數名稱及其值

  [alternateHTML]

  </applet>

  1)不支援Java的浏覽器會把<applet></applet>之間的普通HTML文檔顯示出來;支援Java的浏覽器,則把其中的普通HTML文檔忽略。

  2)AppletViewer僅支援<applet> </applet>标記,其它标記不會被顯示出來。

  經過精心設計,可以使java程式同時是Applet與Application,如下例所示:

 例6.9

   import java.awt.*;

   import java.awt.event.*;

   public class AppletApp extends Applet {

      public void main(String args[]) {

        Frame frame=new Frame("Application"); //構造一個Frame

        AppletApp app=new AppletApp();

        frame.add("Center",app);

        frame.setSize(200,200); //改變Frame的尺寸

   frame.validate();

   frame.setVisible(true); //使Frame可見

   frame.addWindwoListener(new WindowControl(app));

                    //給Frame加入監聽器

   app.init(); //初始化Applet

   app.start(); //運作該Applet

      public void paint(Graphics g) { //重畫方法

        g.drawString("hello world",25,25);

      public void destroy(){

        System.exit(0);

   }

   class WindowControl extends WindowAdapter { //監聽器類

      Applet c;

      public WindowControl(Applet c) { //構造函數

        this.c=c;

      public void windowClosing(WindowEvent e) {

                   //關閉視窗時調用的方法

        c.destroy( );

6.3.2 Applet的AWT繪制

  看圖  

  Applet程式中所采用的AWT的繪圖機制主要涉及三個方法:paint()方法、update()方法和repaint()方法,update()方法和paint()方法都有一個Graphics類參數。Graphics是畫圖的關鍵,它可以支援兩種繪圖:一種是基本的繪圖,如:畫線、矩形、圓等;另一種是畫圖象,主要用于動畫制作。

  要進行繪圖,首先要找到一個Graphics類的對象。update()方法和paint()方法所傳遞的參數都是Graphics類的對象,是以主要是通過重載它們來進行繪圖,這是在動畫程式中經常使用的方法。我們還可以通過getGraphics()方法得到一個Graphics類的對象,這個對象和update()方法和paint()方法中所傳遞的對象一樣,都是該成員所對應的Graphics類的對象。得到了Graphics類的對象,就可使用各種繪圖方法。

  Graphics中提供的圖形繪制方法有:

  paint( ) //進行繪圖的具體操作,必須有程式員重寫

  update( ) //用于更新圖形,先清除背景、前景,再調用paint()

  repaint( ) /*用于重繪圖形,在元件外形發生變化,即大小改變或位置移動時,repaint( )方法立即被系統自動調用,而實際上repaint()方法是自動調用update()方法*/

6.3.3 Applet和浏覽器間的通信

  在Applet類中提供了許多方法,使之可以與浏覽器進行通信。下面對這些方法進行簡要的介紹:

一個Web頁可包含一個以上的小應用程式。一個Web頁中的多個小應用程式可以直接通過java.applet包中提供的方法進行通信。

  getDocumentBase( ) //傳回目前網頁所在的URL

  getCodeBase( ) //傳回目前applet所在的URL

  getImage(URL base,String target) //傳回網址URL中名為target的圖像

  getAudioClip(URL base,String target)

                 //傳回網址URL中名為target的聲音對象

  getParameter(String target ) //提取HTML檔案中名為target的參數的值

  同頁Applet間的通信

  在java.applet.Applet類中提供了如下方法得到目前運作頁的環境上下文AppletContext對象。

  public AppletContext getAppletContext();

  通過AppletContext對象,可以得到目前小應用程式運作環境的資訊。AppletContext是一個接口,其中定義了一些方法可以得到目前頁的其它小應用程式,進而實作同頁小應用程式之間的通信。

  (1)得到目前運作頁的環境上下文AppletContext對象

     public AppletContext getAppletContext();

  (2)取得名為name的Applet對象

     public abstract Applet getApplet(String name);

  (3)得到目前頁中所有Applet對象

     public abstract Enumeration getApplets();

 例6.11

   import java.applet.*;

   import java.util.Enumeration;

   public class GetApplets extends Applet implemements ActionListener {

     private TextArea textArea; //聲明一個TextArea

     private String newline;

     public void init() {

       Button b=new Button("Click to call getApplets()");

       b.addActionListener(this);

       setLayout(new BorderLayout());

     add("North",b);

     textArea=new TextAred(5,40);

     textArea.setEditable(false);

     add("Center",textArea);

     newline=System.getProperty("line,separator");

               //取得系統目前的換行符

     public void actionPerformed(ActionEvent event) {

              /*Button b點選後的事件處理函數*/

       printApplets();

     public String getAppletInfo() {

       return "GetApplets by Dong.li";

   public void printApplets()}

       Enumeration e=getAppletContext().getApplets();

             /*得到目前網頁所有的Applet對象*/

       textArea.append("Results of getApplets():" + newline);

       while(e.hasMoreElements()) {

         Applet applet=(Applet) e.nextElement();

         String info=((Applet)applet).getAppletInfo();

            /*逐個取得目前網頁Applet對象的資訊*/

         if (info!=null) {

           textArea.append("-"+info+newline);

            /*在textArea中輸出網頁所有Applet的資訊*/

         } else {

           textArea.append("-"+applet.getClass().getName()+newline) ;

           textArea.append("__________"+newline+newline);

【本講小結】

  本講介紹了Java的線程和Java Applet的一些基本知識和簡單應用,通過對線程簡介,闡明了線程與程序的差別,通過描述線程的概念模型的基本原理以及線程體的構造方法和應用執行個體,講解了線程的基本特性和線程的不同狀态的轉換關系和調用方法,明确了線程的使用方法,然後,我們又進一步講述了線程的幾種排程政策,在不同的排程政策下優先級的作用。以及如何進行基本的線程的控制,線程的重點和難點在于多線程的互斥與同步,首先我們必須明白互斥鎖的概念和作用,如何使用互斥鎖來控制和處理多線程的同步問題。

  本講的後半部分我們對Java Applet做了介紹和一些基本的應用的講解,例如Applet的建立,生命周期和Applet的主要方法以及Applet的AWT繪制,最後簡單介紹了Applet和浏覽器間的通信的方法。

  下面的方法支援基本的繪圖和畫圖像:

  void drawLine( )

  void drawArc( )

  void drawPolygon( )

  void drawRect( )

  void drawRoundRect( )

  void fill3DRect( )

  void fillOval( )

  java.awt.Graphics類

輸出文字:

  void drawBytes( )

  void drawChars( )

  void drawString( )

  Applet 的AWT繪制舉例如下:

 例6.10

  import java.awt.*;

  import java.awt.event.*;

  import java.applet.*;

  public class ArcTest extends Applet implements WindowListener {

     ArcControls controls;

     pulic void init(){ //Applet的入口方法

       ArcCanvas c=new ArcCanvas();

     Add("Center",c);

     add("South",controls=new ArcControls(C));

  public void start(){

     controls.setEnabled(true); //激活controls

  public void stop(){

     controls.setEnabled(false);

  public void windowActivated(WindowEvent e){ }

            //重寫WindowListener的方法

  public void windowClosed(WindowEvent e){ }

  public void windowClosing(WindowEvent e){

     System.exit(0); }

  public void windowDeactivated(WindowEvent e){}

  public void windowDeiconified(WindowEvent e){}

  public void windowIconified(WindowEvent e){ }

  public void windowOpend(WindowEvent e){ }

  public static void main(String args[]) {

     Frame f=new Frame("ArcTest"); //構造Frame

     ArcTest arcTest=new ArcTest(); //構造arcTest

     atcTest.init();

     arcTest.start();

     f.add("Center",arcTest);

     f.setSize(300,300);

     f.show();

     f.addWindowListener(arcTest);

  class ArcCanvas extends Canvas{ //類ArcCanvas

     int startAngle=0;

     int endAngle=45;

     boolean filled=false;

     Font font;

     public void paint(Graphics g){

        //paint方法,該方法的作用是在Canvas上畫圖

      Rectangle r=getBounds();

      int hlines=r.height/10;

      int vlines=r.width/10;

      g.setColor(Color.pink);

  for(int i=1;i<=hlines;i++) {

      g.drawLine(0,i*10,r.width,i*10);

  for(int i=1;i<=vlines;i++) {

      g.drawLine(i*10,0,i*10,r.height);

  g.setColor(Color.red);

  if(filled) {

      g.fillArc(0,0,r.width-1,r.height-1,startAngle,endAngle); }

      else { g.drawArc(0,0,r.width-1,r.height-1,startAngle, endAngle);

  g.setColor(Color.black);

  g.setFont(font);

  g.drawLine(0,r.height/2,r.width,r.height/2);

  g.drawLine(r.width/2,0,r.width/2,r.height);

  g.drawLine(0,,0,r.width,r.height);

  g.drawLine(r.width,0,0,r.height);

  int sx=10;

  int sy=r.height-28;

  g.drawString("S="+startAngle,sx,sy);

  g.drawString("E="+ednAngle,sx,sy+14);

  public void redraw(boolean filled,int start,int end){ //重畫方法

        this.filled=filled;

        this.startAngle=start;

        this.endAngle=end;

        repaint();

        //通過調用repaint()方法,進而最終調用paint方法完成重畫

  class ArcControls extends Panel implements ActionListener { //ArcControls類

        TextFiled s;

        TextFiled e;

        ArcCanvas canvas;

  public ArcControls(ArcCanvas canvas) {

        Button b=null;

        this.canvas=canvas;

        add(s=new TextField("0",4));

        add(e=new TextField("45",4));

        b=new Button("Fill");

        b.addActionListener(this);

        add(b);

        b=new Button("Draw");

  public void actionPerformed(ActionEvent ev) {

          //實作接口ActionListener的方法

     String label=ev.getActionCommand();

     canvas.redraw(label.equals("Fill"),

     Integer.parseInt(s.getText(),trim()),

     Integer.parserInt(e.getText().trim());

6.1 線程簡介

  随着計算機的飛速發展,個人計算機上的作業系統也紛紛采用多任務和分時設計,将早期隻有大型計算機才具有的系統特性帶到了個人計算機系統中。一般可以在同一時間内執行多個程式的作業系統都有程序的概念。一個程序就是一個執行中的程式,而每一個程序都有自己獨立的一塊記憶體空間、一組系統資源。在程序概念中,每一個程序的内部資料和狀态都是完全獨立的。Java程式通過流控制來執行程式流,程式中單個順序的流控制稱為線程,多線程則指的是在單個程式中可以同時運作多個不同的線程,執行不同的任務。多線程意味着一個程式的多行語句可以看上去幾乎在同一時間内同時運作。

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

  一個線程是一個程式内部的順序控制流。

  1. 程序:每個程序都有獨立的代碼和資料空間(程序上下文) ,程序切換的開銷大。

  2. 線程:輕量的程序,同一類線程共享代碼和資料空間,每個線程有獨立的運作棧和程式計數器(PC),線程切換的開銷小。

  3. 多程序:在作業系統中,能同時運作多個任務程式。

  4. 多線程:在同一應用程式中,有多個順序流同時執行。

6.1.1 線程的概念模型

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

  1. 虛拟的CPU,封裝在java.lang.Thread類中。

  2. CPU所執行的代碼,傳遞給Thread類。

  3. CPU所處理的資料,傳遞給Thread類。

圖6.1線程

6. 1. 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( )稱為線程體。圖6.2表示了java線程的不同狀态以及狀态之間轉換所調用的方法。

圖6.2 線程的狀态

    

1. 建立狀态(new Thread)

  執行下列語句時,線程就處于建立狀态:

  Thread myThread = new MyThreadClass( );

  當一個線程處于建立狀态時,它僅僅是一個空的線程對象,系統不為它配置設定資源。

2. 可運作狀态( Runnable )

  myThread.start( );

  當一個線程處于可運作狀态時,系統為這個線程配置設定了它需的系統資源,安排其運作并調用線程運作方法,這樣就使得該線程處于可運作( Runnable )狀态。需要注意的是這一狀态并不是運作中狀态(Running ),因為線程也許實際上并未真正運作。由于很多計算機都是單處理器的,是以要在同一時刻運作所有的處于可運作狀态的線程是不可能的,Java的運作系統必須實作排程來保證這些線程共享處理器。

3. 不可運作狀态(Not Runnable)

  進入不可運作狀态的原因有如下幾條:

  1) 調用了sleep()方法;

  2) 調用了suspend()方法;

  3) 為等候一個條件變量,線程調用wait()方法;

  4) 輸入輸出流中發生線程阻塞;

  不可運作狀态也稱為阻塞狀态(Blocked)。因為某種原因(輸入/輸出、等待消息或其它阻塞情況),系統不能執行線程的狀态。這時即使處理器空閑,也不能執行該線程。

4. 死亡狀态(Dead)

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

6. 1. 2 線程體(2)

◇線程體的構造

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

例6.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()方法是學習線程的一個關鍵。

 例6.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);//顯示目前時間