程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 線程為何會堵塞

線程為何會堵塞

編輯:關於JAVA

堵塞狀態是前述四種狀態中最有趣的,值得我們作進一步的探討。線程被堵塞可能是由下述五方面的原因造成的:
(1) 調用sleep(毫秒數),使線程進入“睡眠”狀態。在規定的時間內,這個線程是不會運行的。
(2) 用suspend()暫停了線程的執行。除非線程收到resume()消息,否則不會返回“可運行”狀態。
(3) 用wait()暫停了線程的執行。除非線程收到nofify()或者notifyAll()消息,否則不會變成“可運行”(是的,這看起來同原因2非常相象,但有一個明顯的區別是我們馬上要揭示的)。
(4) 線程正在等候一些IO(輸入輸出)操作完成。
(5) 線程試圖調用另一個對象的“同步”方法,但那個對象處於鎖定狀態,暫時無法使用。

亦可調用yield()(Thread類的一個方法)自動放棄CPU,以便其他線程能夠運行。然而,假如調度機制覺得我們的線程已擁有足夠的時間,並跳轉到另一個線程,就會發生同樣的事情。也就是說,沒有什麼能防止調度機制重新啟動我們的線程。線程被堵塞後,便有一些原因造成它不能繼續運行。
下面這個例子展示了進入堵塞狀態的全部五種途徑。它們全都存在於名為Blocking.java的一個文件中,但在這兒采用散落的片斷進行解釋(大家可注意到片斷前後的“Continued”以及“Continuing”標志。利用第17章介紹的工具,可將這些片斷連結到一起)。首先讓我們看看基本的框架:
 

//: Blocking.java
// Demonstrates the various ways a thread
// can be blocked.
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.io.*;

//////////// The basic framework ///////////
class Blockable extends Thread {
  private Peeker peeker;
  protected TextField state = new TextField(40);
  protected int i;
  public Blockable(Container c) {
    c.add(state);
    peeker = new Peeker(this, c);
  }
  public synchronized int read() { return i; }
  protected synchronized void update() {
    state.setText(getClass().getName()
      + " state: i = " + i);
  }
  public void stopPeeker() { 
    // peeker.stop(); Deprecated in Java 1.2
    peeker.terminate(); // The preferred approach
  }
}

class Peeker extends Thread {
  private Blockable b;
  private int session;
  private TextField status = new TextField(40);
  private boolean stop = false;
  public Peeker(Blockable b, Container c) {
    c.add(status);
    this.b = b;
    start();
  }
  public void terminate() { stop = true; }
  public void run() {
    while (!stop) {
      status.setText(b.getClass().getName()
        + " Peeker " + (++session)
        + "; value = " + b.read());
       try {
        sleep(100);
      } catch (InterruptedException e){}
    }
  }
} ///:Continued


Blockable類打算成為本例所有類的一個基礎類。一個Blockable對象包含了一個名為state的TextField(文本字段),用於顯示出對象有關的信息。用於顯示這些信息的方法叫作update()。我們發現它用getClass.getName()來產生類名,而不是僅僅把它打印出來;這是由於update(0不知道自己為其調用的那個類的准確名字,因為那個類是從Blockable衍生出來的。
在Blockable中,變動指示符是一個int i;衍生類的run()方法會為其增值。
針對每個Bloackable對象,都會啟動Peeker類的一個線程。Peeker的任務是調用read()方法,檢查與自己關聯的Blockable對象,看看i是否發生了變化,最後用它的status文本字段報告檢查結果。注意read()和update()都是同步的,要求對象的鎖定能自由解除,這一點非常重要。

1. 睡眠
這個程序的第一項測試是用sleep()作出的:
 

///:Continuing
///////////// Blocking via sleep() ///////////
class Sleeper1 extends Blockable {
  public Sleeper1(Container c) { super(c); }
  public synchronized void run() {
    while(true) {
      i++;
      update();
       try {
        sleep(1000);
      } catch (InterruptedException e){}
    }
  }
}
  
class Sleeper2 extends Blockable {
  public Sleeper2(Container c) { super(c); }
  public void run() {
    while(true) {
      change();
       try {
        sleep(1000);
      } catch (InterruptedException e){}
    }
  }
  public synchronized void change() {
      i++;
      update();
  }
} ///:Continued


在Sleeper1中,整個run()方法都是同步的。我們可看到與這個對象關聯在一起的Peeker可以正常運行,直到我們啟動線程為止,隨後Peeker便會完全停止。這正是“堵塞”的一種形式:因為Sleeper1.run()是同步的,而且一旦線程啟動,它就肯定在run()內部,方法永遠不會放棄對象鎖定,造成Peeker線程的堵塞。
Sleeper2通過設置不同步的運行,提供了一種解決方案。只有change()方法才是同步的,所以盡管run()位於sleep()內部,Peeker仍然能訪問自己需要的同步方法——read()。在這裡,我們可看到在啟動了Sleeper2線程以後,Peeker會持續運行下去。

2. 暫停和恢復
這個例子接下來的一部分引入了“掛起”或者“暫停”(Suspend)的概述。Thread類提供了一個名為suspend()的方法,可臨時中止線程;以及一個名為resume()的方法,用於從暫停處開始恢復線程的執行。顯然,我們可以推斷出resume()是由暫停線程外部的某個線程調用的。在這種情況下,需要用到一個名為Resumer(恢復器)的獨立類。演示暫停/恢復過程的每個類都有一個相關的恢復器。如下所示:
 

///:Continuing
/////////// Blocking via suspend() ///////////
class SuspendResume extends Blockable {
  public SuspendResume(Container c) {
    super(c);    
    new Resumer(this); 
  }
}

class SuspendResume1 extends SuspendResume {
  public SuspendResume1(Container c) { super(c);}
  public synchronized void run() {
    while(true) {
      i++;
      update();
      suspend(); // Deprecated in Java 1.2
    }
  }
}

class SuspendResume2 extends SuspendResume {
  public SuspendResume2(Container c) { super(c);}
  public void run() {
    while(true) {
      change();
      suspend(); // Deprecated in Java 1.2
    }
  }
  public synchronized void change() {
      i++;
      update();
  }
}

class Resumer extends Thread {
  private SuspendResume sr;
  public Resumer(SuspendResume sr) {
    this.sr = sr;
    start();
  }
  public void run() {
    while(true) {
       try {
        sleep(1000);
      } catch (InterruptedException e){}
      sr.resume(); // Deprecated in Java 1.2
    }
  }
} ///:Continued


SuspendResume1也提供了一個同步的run()方法。同樣地,當我們啟動這個線程以後,就會發現與它關聯的Peeker進入“堵塞”狀態,等候對象鎖被釋放,但那永遠不會發生。和往常一樣,這個問題在SuspendResume2裡得到了解決,它並不同步整個run()方法,而是采用了一個單獨的同步change()方法。
對於Java 1.2,大家應注意suspend()和resume()已獲得強烈反對,因為suspend()包含了對象鎖,所以極易出現“死鎖”現象。換言之,很容易就會看到許多被鎖住的對象在傻乎乎地等待對方。這會造成整個應用程序的“凝固”。盡管在一些老程序中還能看到它們的蹤跡,但在你寫自己的程序時,無論如何都應避免。本章稍後就會講述正確的方案是什麼。

3. 等待和通知
通過前兩個例子的實踐,我們知道無論sleep()還是suspend()都不會在自己被調用的時候解除鎖定。需要用到對象鎖時,請務必注意這個問題。在另一方面,wait()方法在被調用時卻會解除鎖定,這意味著可在執行wait()期間調用線程對象中的其他同步方法。但在接著的兩個類中,我們看到run()方法都是“同步”的。在wait()期間,Peeker仍然擁有對同步方法的完全訪問權限。這是由於wait()在掛起內部調用的方法時,會解除對象的鎖定。
我們也可以看到wait()的兩種形式。第一種形式采用一個以毫秒為單位的參數,它具有與sleep()中相同的含義:暫停這一段規定時間。區別在於在wait()中,對象鎖已被解除,而且能夠自由地退出wait(),因為一個notify()可強行使時間流逝。
第二種形式不采用任何參數,這意味著wait()會持續執行,直到notify()介入為止。而且在一段時間以後,不會自行中止。
wait()和notify()比較特別的一個地方是這兩個方法都屬於基礎類Object的一部分,不象sleep(),suspend()以及resume()那樣屬於Thread的一部分。盡管這表面看有點兒奇怪——居然讓專門進行線程處理的東西成為通用基礎類的一部分——但仔細想想又會釋然,因為它們操縱的對象鎖也屬於每個對象的一部分。因此,我們可將一個wait()置入任何同步方法內部,無論在那個類裡是否准備進行涉及線程的處理。事實上,我們能調用wait()的唯一地方是在一個同步的方法或代碼塊內部。若在一個不同步的方法內調用wait()或者notify(),盡管程序仍然會編譯,但在運行它的時候,就會得到一個IllegalMonitorStateException(非法監視器狀態違例),而且會出現多少有點莫名其妙的一條消息:“current thread not owner”(當前線程不是所有人”。注意sleep(),suspend()以及resume()都能在不同步的方法內調用,因為它們不需要對鎖定進行操作。
只能為自己的鎖定調用wait()和notify()。同樣地,仍然可以編譯那些試圖使用錯誤鎖定的代碼,但和往常一樣會產生同樣的IllegalMonitorStateException違例。我們沒辦法用其他人的對象鎖來愚弄系統,但可要求另一個對象執行相應的操作,對它自己的鎖進行操作。所以一種做法是創建一個同步方法,令其為自己的對象調用notify()。但在Notifier中,我們會看到一個同步方法內部的notify():
 

synchronized(wn2) {
  wn2.notify();
}


其中,wn2是類型為WaitNotify2的對象。盡管並不屬於WaitNotify2的一部分,這個方法仍然獲得了wn2對象的鎖定。在這個時候,它為wn2調用notify()是合法的,不會得到IllegalMonitorStateException違例。
 

///:Continuing
/////////// Blocking via wait() ///////////
class WaitNotify1 extends Blockable {
  public WaitNotify1(Container c) { super(c); }
  public synchronized void run() {
    while(true) {
      i++;
      update();
       try {
        wait(1000);
      } catch (InterruptedException e){}
    }
  }
}

class WaitNotify2 extends Blockable {
  public WaitNotify2(Container c) {
    super(c);
    new Notifier(this); 
  }
  public synchronized void run() {
    while(true) {
      i++;
      update();
       try {
        wait();
      } catch (InterruptedException e){}
    }
  }
}

class Notifier extends Thread {
  private WaitNotify2 wn2;
  public Notifier(WaitNotify2 wn2) {
    this.wn2 = wn2;
    start();
  }
  public void run() {
    while(true) {
       try {
        sleep(2000);
      } catch (InterruptedException e){}
      synchronized(wn2) {
        wn2.notify();
      }
    }
  }
} ///:Continued


若必須等候其他某些條件(從線程外部加以控制)發生變化,同時又不想在線程內一直傻乎乎地等下去,一般就需要用到wait()。wait()允許我們將線程置入“睡眠”狀態,同時又“積極”地等待條件發生改變。而且只有在一個notify()或notifyAll()發生變化的時候,線程才會被喚醒,並檢查條件是否有變。因此,我們認為它提供了在線程間進行同步的一種手段。

4. IO堵塞
若一個數據流必須等候一些IO活動,便會自動進入“堵塞”狀態。在本例下面列出的部分中,有兩個類協同通用的Reader以及Writer對象工作(使用Java 1.1的流)。但在測試模型中,會設置一個管道化的數據流,使兩個線程相互間能安全地傳遞數據(這正是使用管道流的目的)。
Sender將數據置入Writer,並“睡眠”隨機長短的時間。然而,Receiver本身並沒有包括sleep(),suspend()或者wait()方法。但在執行read()的時候,如果沒有數據存在,它會自動進入“堵塞”狀態。如下所示:
 

///:Continuing
class Sender extends Blockable { // send
  private Writer out;
  public Sender(Container c, Writer out) { 
    super(c);
    this.out = out; 
  }
  public void run() {
    while(true) {
      for(char c = 'A'; c <= 'z'; c++) {
         try {
          i++;
          out.write(c);
          state.setText("Sender sent: " 
            + (char)c);
          sleep((int)(3000 * Math.random()));
        } catch (InterruptedException e){}
          catch (IOException e) {}
      }
    }
  }
}

class Receiver extends Blockable {
  private Reader in;
  public Receiver(Container c, Reader in) { 
    super(c);
    this.in = in; 
  }
  public void run() {
    try {
      while(true) {
        i++; // Show peeker it's alive
        // Blocks until characters are there:
        state.setText("Receiver read: "
          + (char)in.read());
      }
    } catch(IOException e) { e.printStackTrace();}
  }
} ///:Continued


這兩個類也將信息送入自己的state字段,並修改i值,使Peeker知道線程仍在運行。

5. 測試
令人驚訝的是,主要的程序片(Applet)類非常簡單,這是大多數工作都已置入Blockable框架的緣故。大概地說,我們創建了一個由Blockable對象構成的數組。而且由於每個對象都是一個線程,所以在按下“start”按鈕後,它們會采取自己的行動。還有另一個按鈕和actionPerformed()從句,用於中止所有Peeker對象。由於Java 1.2“反對”使用Thread的stop()方法,所以可考慮采用這種折衷形式的中止方式。
為了在Sender和Receiver之間建立一個連接,我們創建了一個PipedWriter和一個PipedReader。注意PipedReader in必須通過一個構建器參數同PipedWriterout連接起來。在那以後,我們在out內放進去的所有東西都可從in中提取出來——似乎那些東西是通過一個“管道”傳輸過去的。隨後將in和out對象分別傳遞給Receiver和Sender構建器;後者將它們當作任意類型的Reader和Writer看待(也就是說,它們被“上溯”造型了)。
Blockable句柄b的數組在定義之初並未得到初始化,因為管道化的數據流是不可在定義前設置好的(對try塊的需要將成為障礙):
 

///:Continuing
/////////// Testing Everything ///////////
public class Blocking extends Applet {
  private Button 
    start = new Button("Start"),
    stopPeekers = new Button("Stop Peekers");
  private boolean started = false;
  private Blockable[] b;
  private PipedWriter out;
  private PipedReader in;
  public void init() {
     out = new PipedWriter();
    try {
      in = new PipedReader(out);
    } catch(IOException e) {}
    b = new Blockable[] {
      new Sleeper1(this),
      new Sleeper2(this),
      new SuspendResume1(this),
      new SuspendResume2(this),
      new WaitNotify1(this),
      new WaitNotify2(this),
      new Sender(this, out),
      new Receiver(this, in)
    };
    start.addActionListener(new StartL());
    add(start);
    stopPeekers.addActionListener(
      new StopPeekersL());
    add(stopPeekers);
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(!started) {
        started = true;
        for(int i = 0; i < b.length; i++)
          b[i].start();
      }
    }
  }
  class StopPeekersL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      // Demonstration of the preferred 
      // alternative to Thread.stop():
      for(int i = 0; i < b.length; i++)
        b[i].stopPeeker();
    }
  }
  public static void main(String[] args) {
    Blocking applet = new Blocking();
    Frame aFrame = new Frame("Blocking");
    aFrame.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    aFrame.add(applet, BorderLayout.CENTER);
    aFrame.setSize(350,550);
    applet.init();
    applet.start();
    aFrame.setVisible(true);
  }
} ///:~


在init()中,注意循環會遍歷整個數組,並為頁添加state和peeker.status文本字段。
首次創建好Blockable線程以後,每個這樣的線程都會自動創建並啟動自己的Peeker。所以我們會看到各個Peeker都在Blockable線程啟動之前運行起來。這一點非常重要,因為在Blockable線程啟動的時候,部分Peeker會被堵塞,並停止運行。弄懂這一點,將有助於我們加深對“堵塞”這一概念的認識。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved