返回介绍

14.3.1 为何会堵塞

发布于 2024-10-15 23:56:32 字数 11825 浏览 0 评论 0 收藏 0

堵塞状态是前述四种状态中最有趣的,值得我们作进一步的探讨。线程被堵塞可能是由下述五方面的原因造成的:

(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 会被堵塞,并停止运行。弄懂这一点,将有助于我们加深对“堵塞”这一概念的认识。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文