Skip to content

7. 执行线程

7.1. 简介

当启动一个应用程序时,它会在一个称为线程的执行流中运行。用于建模 thread 的类是 java.lang.Thread 类,以下是该类的部分属性和方法:

currentThread()
返回当前正在执行的线程
setName()
设置线程名称
getName()
线程名称
isAlive()
指示线程是否处于活动状态(true)或非活动状态(false)
start()
启动线程的执行
run()
在前一个 start 方法执行后自动执行的方法
sleep(n)
暂停线程的执行 n 毫秒
join()
阻塞操作——等待线程结束再执行下一条指令

最常用的构造函数如下:

Thread()
创建对异步任务的引用。该任务目前仍处于非活动状态。创建的任务必须具有 run 方法:通常会使用从 Thread 派生的类。
Thread(Runnable object)
同上,但作为参数传递的对象Runnable实现了方法run。

让我们来看一个简单的示例,该示例突出了主执行线程的存在,即类中 main 函数的执行线程:

// 使用线程

import java.io.*;
import java.util.*;

public class thread1{
    public static void main(String[] arg)throws Exception {
         // 初始化当前线程
        Thread main=Thread.currentThread();
         // 显示
        System.out.println("Thread courant : " + main.getName());
         // 更改名称
        main.setName("myMainThread");
         // 验证
        System.out.println("Thread courant : " + main.getName());

         // 无限循环
        while(true){
        // 获取时间
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
             // 显示
            System.out.println(main.getName() + " : " +H);
             // 临时停止
            Thread.sleep(1000);
        }//while
    }//main
}//类

屏幕显示结果:


Thread courant : main
Thread courant : myMainThread
myMainThread : 15:34:9
myMainThread : 15:34:10
myMainThread : 15:34:11
myMainThread : 15:34:12
Terminer le programme de commandes (O/N) ? o

上例说明了以下几点:

  • 函数 main 确实在单线程中运行
  • 可通过 Thread.currentThread() 访问该线程的属性
  • sleep 方法的作用。在此示例中,执行 main 的线程会在两次显示之间定期休眠 1 秒。

7.2. 创建执行线程

某些应用程序中,代码片段可能在不同的执行线程中“同时”运行。当我们说 thread 同时运行时,通常是一种措辞上的误用。 如果机器只有一个处理器(这种情况至今仍很常见),那么 thread 实例将共享该处理器:它们轮流在短暂的时间内(几毫秒)使用它。这正是产生执行并行性错觉的原因。 分配给某个 thread 的时间段取决于多种因素,其中包括其优先级——该优先级虽有默认值,但也可通过编程设定。当一个 thread 获得处理器控制权时,通常会将其使用至分配时间的结束。不过,它也可能提前释放处理器:

  • 进入事件等待状态(wait, join
  • 进入预定时长的休眠状态(sleep
  • 线程 T 可以通过多种方式创建
    • 继承类 Thread 并重写其方法 run
    • 在类中实现 Runnable 接口,并使用 new Thread(Runnable) 构造函数。 Runnable 是一个仅定义了一个方法的接口:public void run()。因此,上述构造函数的参数是任何实现该方法 run 的类实例。

在下面的示例中,线程是通过继承自 Thread 类的匿名类创建的:

            // 创建线程 i
            tâches[i]=new Thread() {
          public void run() {
            affiche();
        }
      };//任务定义[i]

方法 run 在此仅调用方法 affiche

  • 线程 T 的执行由 T.start() 启动:该方法属于 Thread 类,执行若干初始化操作后,会自动调用该线程或接口 Runnable 中的 run 方法。 执行 T.start() 语句的程序不会等待任务 T 结束:它会立即跳转到下一条语句。此时,有两个任务正在并行执行。它们通常需要相互通信,以了解共同工作的进展情况。这就是线程同步的问题。
  • 一旦启动,thread 便会自主运行。当其执行的函数 run 完成工作时,它才会停止。
  • 我们可以等待线程 T 通过 T.join() 完成执行。这里使用了一条阻塞指令:执行该指令的程序将被阻塞,直到任务 T 完成工作。这也是一种同步方式。

让我们来看一下以下程序:

// 线程的使用

import java.io.*;
import java.util.*;

public class thread2{
    public static void main(String[] arg) {
         // 初始化当前线程
        Thread main=Thread.currentThread();
         // 为当前线程命名
        main.setName("myMainThread");
         // 主函数开始
        System.out.println("début du thread " +main.getName());

         // 创建执行线程
        Thread[] tâches=new Thread[5];
        for(int i=0;i<tâches.length;i++){
             // 创建线程 i
            tâches[i]=new Thread() {
          public void run() {
            affiche();
        }
      };//任务定义[i]
             // 设置线程名称
            tâches[i].setName(""+i);
             // 启动线程 i 的执行
            tâches[i].start();
        }//for

         // 主程序结束
        System.out.println("fin du thread " +main.getName());
    }//主程序

    public static void affiche() {
         // 获取时间
    Calendar calendrier=Calendar.getInstance();
    String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
         // 显示开始执行
        System.out.println("Début d'exécution de la méthode affiche dans le Thread " + 
        Thread.currentThread().getName()+ " : " + H);
         // 休眠 1 秒
        try{
        Thread.sleep(1000);
    }catch (Exception ex){}
     // 获取时间
    calendrier=Calendar.getInstance();    
    H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
         // 显示执行结束
        System.out.println("Fin d'exécution de la méthode affiche dans le Thread " 
    +Thread.currentThread().getName()+ " : " + H);
    }// 显示
}//类

主线程(即执行函数 main 的线程)创建了另外 5 个线程,负责执行静态方法 affiche。结果如下:


début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 15:48:3
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 1 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 2 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 3 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 4 : 15:48:3
Fin d'exécution de la méthode affiche dans le Thread 0 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 1 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 2 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 3 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 4 : 15:48:4

这些结果非常有启发性:

  • 首先可以看出,线程的启动并不阻塞。方法 main 并行启动了 5 个线程,并在它们之前完成了自身执行。操作
             // 启动线程 i 的执行
            tâches[i].start();

会启动线程 tâches[i] 的执行,但启动完成后,执行会立即继续执行下一条语句,而不会等待该线程执行完毕。

  • 所有创建的线程都必须执行方法 affiche。执行顺序是不可预测的。即使在示例中,执行顺序似乎遵循了线程的启动顺序,也不能据此得出普遍结论。 此处操作系统拥有 6 个线程和 1 个处理器。它将根据自身的规则将处理器分配给这 6 个线程。
  • 从结果中可以看到方法 sleep 的执行效果。在该示例中,是线程 0 最先执行方法 affiche。 执行开始消息显示后,它执行方法 sleep,该方法使其暂停 1 秒。随后它失去处理器,处理器因此可供其他线程使用。示例显示线程 1 将获得该处理器。 线程 1 将遵循与其他线程相同的流程。当线程 0 的 1 秒休眠结束时,其执行即可恢复。系统将处理器分配给它,它便能完成方法 affiche 的执行。

让我们修改程序,用以下指令结束方法 main

         // 主线程结束
        System.out.println("fin du thread " +main.getName());
     // 应用程序停止
    System.exit(0);

运行新程序的结果如下:


début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 1 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 2 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 3 : 16:5:45
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 4 : 16:5:45

一旦方法 main 执行以下语句:

    System.exit(0);

它会停止应用程序中的所有线程,而不仅仅是 main 线程。 方法 main 可能希望在自身结束之前,等待其创建的线程执行完毕。这可以通过 Thread 类的 join 方法实现:

     // 等待所有线程
        for(int i=0;i<tâches.length;i++){
            // 等待线程 i
            tâches[i].join();
    }//for  

         // 主线程结束
        System.out.println("fin du thread " +main.getName());
     // 应用程序停止
    System.exit(0);

由此得到以下结果:

début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 1 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 2 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 3 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 4 : 16:11:9
Fin d'exécution de la méthode affiche dans le Thread 0 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 1 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 2 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 3 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 4 : 16:11:10
fin du thread myMainThread

7.3. 线程的意义

既然我们已经指出了默认线程的存在——即执行方法 Main 的那个线程——并且我们知道如何创建其他线程,那么让我们来探讨一下线程对我们的意义,以及我们为何在此介绍它们。 有一种应用程序非常适合使用线程,那就是互联网上的客户端-服务器应用程序。 在此类应用程序中,位于 S1 机器上的服务器会响应位于远程机器 C1、C2、……、Cn 上的客户端的请求。

Image

我们每天都在使用符合这一模式的互联网应用:Web服务、电子邮件、论坛浏览、文件传输……在上图中,服务器S1需要同时为客户端Ci提供服务。 以服务器 FTP(文件传输协议)为例,它向客户端提供文件,我们知道一次文件传输有时可能需要数小时。当然,绝不能让一个客户端独自独占服务器这么长时间。 通常的做法是,服务器创建与客户端数量相等的执行线程。每个线程负责处理一个特定的客户端。由于处理器在机器上所有活动线程之间循环分配,服务器因此能与每个客户端进行短暂交互,从而确保服务的并发性。

Image

7.4. 图形时钟

考虑以下应用程序,它会显示一个包含时钟和用于停止或重启时钟的按钮的窗口:

要使时钟正常运行,必须有一个进程负责每秒更新时间。同时,还需监听窗口中发生的事件:当用户点击“停止”按钮时,必须停止时钟。这里有两个并行且异步的任务:用户可能在任何时候点击按钮。

假设此时计时器尚未启动,用户点击了“开始”按钮。这属于典型的事件处理场景,人们可能会认为由运行该窗口的线程中的某个方法来管理计时器即可。 然而,当图形应用程序的方法正在执行时,该应用程序的线程便不再监听图形界面的事件。这些事件发生后会被放入队列中,待当前正在执行的方法完成后再由应用程序处理。 在我们的时钟示例中,该方法将始终处于运行状态,因为只有点击“停止”按钮才能终止它。然而,该事件只有在方法执行完毕后才会被处理。这形成了一个死循环。

解决此问题的方案是:当用户点击“启动”按钮时,启动一个任务来管理时钟,同时应用程序仍能继续监听窗口中发生的事件。这样,将有两个独立的任务并行执行:

  • 时钟管理
  • 监听窗口事件

让我们回到我们的图形时钟:

编号
类型
名称
角色
1
JTextField (可编辑=false)
txtHorloge
显示时间
2
JButton
btnGoStop
停止或启动时钟

使用 JBuilder 构建的应用程序的实用代码如下:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;

public class interfaceHorloge extends JFrame {
  JPanel contentPane;
  JTextField txtHorloge = new JTextField();
  JButton btnGoStop = new JButton();

  // 实例属性
  boolean finHorloge=true;

   //构建框架
  public interfaceHorloge() {
    enableEvents(AWTEvent.WINDOW_EVENT_MASK);
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
  }

  private void runHorloge(){
    // 在未收到停止指令前循环
    while( ! finHorloge){
       // 获取时间
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // 将其显示在 T 字段中
      txtHorloge.setText(H);
       // 等待一秒
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // 出现错误
        System.exit(1);
      }//try
    }// while
  }// runHorloge

   //初始化组件
  private void jbInit() throws Exception  {
...................
  }

   //已替换,因此窗口关闭时我们可以退出
  protected void processWindowEvent(WindowEvent e) {
.............
  }

  void btnGoStop_actionPerformed(ActionEvent e) {
    // 启动/停止时钟
     // 获取按钮的标签
    String libellé=btnGoStop.getText();
    // 启动?
    if(libellé.equals("Lancer")){
      // 创建将运行时钟的线程
      Thread thHorloge=new Thread(){
        public void run(){
          runHorloge();
        }
      };//定义线程
       // 允许线程启动
      finHorloge=false;
       // 更改按钮的标签
      btnGoStop.setText("Arrêter");
      // 启动线程
      thHorloge.start();
      // 结束
      return;
    }//if
    // 停止
    if(libellé.equals("Arrêter")){
      // 通知线程停止
      finHorloge=true;
       // 更改按钮的标签
      btnGoStop.setText("Lancer");
       // 结束
      return;
    }//if
  }
} 

当用户点击“运行”按钮时,将使用一个匿名类创建一个线程:

      Thread thHorloge=new Thread(){
        public void run(){
          runHorloge();
        }

该线程的 run 方法调用了应用程序的 runHorloge 方法。完成此操作后,线程即被启动:

      // 启动线程
      thHorloge.start();

随后将执行方法 runHorloge

  private void runHorloge(){
    // 在未收到停止指令前循环执行
    while( ! finHorloge){
       // 获取当前时间
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // 将其显示在 T 字段中
      txtHorloge.setText(H);
       // 等待一秒
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // 输出时出现错误
        System.exit(1);
      }//尝试
    }// while
  }// runHorloge

该方法的工作原理如下:

  1. 在文本框中显示当前时间 txtHorloge
  2. 暂停 1 秒
  3. 重新执行步骤1,但需先检测布尔变量finHorloge(当用户点击按钮Arrêter时,该变量将被设为真)。

7.5. 时钟小程序

我们通过常规方法将前面的图形应用程序转换为小程序,并创建以下文档 HTML appletHorloge.htm

<html>
  <head>
    <title>Applet Horloge</title>
  </head>
  <body>
      <h2>Une applet horloge</h2>
    <applet
      code="appletHorloge.class"
      width="150"
      height="130"
    ></applet>
    </center>
  </body>
</html>

当我们双击该文档将其直接加载到 IE 中时,会看到以下显示:

Image

在此示例中,小程序所需的所有元素均位于同一文件夹内:

E:\data\serge\Jbuilder\horloge\1>dir
13/06/2002  12:17                3 174 appletHorloge.class
13/06/2002  12:17                  658 appletHorloge$1.class
13/06/2002  12:17                  512 appletHorloge$2.class
13/06/2002  12:20                  245 appletHorloge.htm

我们的小程序可以进一步改进。我们提到,在加载小程序时,会先执行方法 init,然后如果存在方法 start,则执行该方法。 此外,当用户离开页面时,如果存在方法 stop,则会执行该方法。当用户返回小程序页面时,将再次调用方法 start。 当小程序使用视觉动画线程时,通常会调用该小程序的 startstop 方法来启动和停止线程。因为当动画被隐藏时,视觉动画线程在后台继续运行是没有意义的。

因此,我们在 Applet 中添加了以下方法 startstop

  public void stop(){
    // 页面被隐藏
     // 跟踪
    System.out.println("Page stop");
     // 页面被隐藏 - 停止线程
    finHorloge=true;
  }

  public void start(){
     // 页面重新显示
     // 跟进
    System.out.println("Page start");
     // 如有必要,重新启动一个新的定时线程
    if(btnGoStop.getText().equals("Arrêter")){
      // 更改标签
      btnGoStop.setText("Lancer");
       // 并模拟用户点击操作
      btnGoStop_actionPerformed(null);
    }//if
  }//start

此外,我们在线程的 run 方法中添加了跟踪代码,以便了解其何时启动和停止:

  private void runHorloge(){
    // 跟踪
    System.out.println("Thread horloge lancé");
    // 在未收到停止指令前循环执行
    while( ! finHorloge){
       // 获取当前时间
      Calendar calendrier=Calendar.getInstance();
      String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
      +calendrier.get(Calendar.MINUTE)+":"
      +calendrier.get(Calendar.SECOND);
       // 将其显示在 T 字段中
      txtHorloge.setText(H);
       // 等待一秒
      try{
        Thread.sleep(1000);
      } catch (Exception e){
        // 报错退出
        return;
      }//try
    }// while
    // 跟踪
    System.out.println("Thread horloge terminé");
  }// runHorloge

现在我们使用 AppletViewer 运行该小程序:

E:\data\serge\Jbuilder\horloge\1>appletviewer appletHorloge.htm
Page start    // 小程序已启动 - 页面已显示
Thread horloge lancé    // 线程已相应启动
Page stop    // 小程序已最小化
Thread horloge terminé    // 相应地停止线程
Page start    // 小程序重新显示
Thread horloge lancé    // 线程已重新启动
Thread horloge terminé    // 点击停止按钮
Thread horloge lancé    // 点击启动按钮
Page stop    // 小程序缩略图    
Thread horloge terminé    // 线程随之停止
Page start    // 重新显示小程序
Thread horloge lancé    // 相应地重新启动线程

对于 AppletViewer,当 AppletViewer 窗口处于可见状态时会触发 start 事件;而将其最小化时则会触发 stop 事件。 上述结果表明,当文档 HTML 被隐藏时,如果线程处于活动状态,则该线程确实会被停止。

7.6. 任务同步

在之前的示例中,有两个任务:

  • 由应用程序本身代表的主任务
  • 负责时钟的任务

两个任务之间的协调由主任务负责,它通过设置一个布尔变量来停止时钟线程。现在我们将探讨任务对公共资源的并发访问问题,该问题也被称为“资源共享”。为了说明这一点,我们将首先研究一个示例。

7.6.1. 非同步计数

考虑以下图形界面:

Image

编号
类型
名称
角色
1
JTextField
txtAGénérer
表示要生成的线程数
2
JTextfield
(不可编辑)
txtGénéres
表示生成的线程数
3
JTextField
(不可编辑)
txtStatus
提供有关遇到的错误以及应用程序本身的详细信息
4
JButton
btnGénérer
启动线程生成

该应用程序的工作原理如下:

  • 用户在字段1中输入要生成的线程数
  • 通过按钮4启动线程生成
  • 线程读取字段2的值,将其递增并显示新值。初始时该字段的值为0。

生成的线程共享一个资源:字段2的值。我们在此旨在展示此类情况下可能遇到的问题。以下是一个运行示例:

Image

可以看到,虽然请求生成1000个线程,但实际只统计到了7个。应用程序的有效代码如下:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class interfaceSynchro extends JFrame {
  JPanel contentPane;
  JLabel jLabel1 = new JLabel();
  JTextField txtAGénérer = new JTextField();
  JButton btnGénérer = new JButton();
  JTextField txtStatus = new JTextField();
  JTextField txtGénérés = new JTextField();
  JLabel jLabel2 = new JLabel();

   // 实例变量
    Thread[] tâches=null;   // 线程
    int[] compteurs=null;   // 计数器

   //构建框架
  public interfaceSynchro() {
..........
  }

   //初始化组件
  private void jbInit() throws Exception  {
......................
  }

   //已替换,因此窗口关闭时我们可以退出
  protected void processWindowEvent(WindowEvent e) {
..................
  }

  void btnGénérer_actionPerformed(ActionEvent e) {
     //生成线程

     // 读取要生成的线程数
    int nbThreads=0;
    try{
      // 读取包含线程数量的字段
      nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
      // 正数 >
      if(nbThreads<=0) throw new Exception();
    }catch(Exception ex){
       //错误
      txtStatus.setText("Nombre invalide");
       // 重新开始
      txtAGénérer.requestFocus();
      return;
    }//捕获

     // 初始时未生成线程
    txtGénérés.setText("0");  // 任务计数器为 0

     // 生成并启动线程
    tâches=new Thread[nbThreads];
    compteurs=new int[nbThreads];
    for(int i=0;i<tâches.length;i++){
      // 创建线程 i
      tâches[i]=new Thread() {
          public void run() {
          incrémente();
        }
      };//线程 i
       // 定义其名称
      tâches[i].setName(""+i);
      // 启动其执行
      tâches[i].start();
    }//for
  }//生成

   // 递增
  private void incrémente(){
     // 获取线程编号
    int iThread=0;
    try{
      iThread=Integer.parseInt(Thread.currentThread().getName());
    }catch(Exception ex){}
     // 读取任务计数器的值
    try{
      compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
    } catch (Exception e){}
     // 将其递增
    compteurs[iThread]++;

     // 等待 100 毫秒——此时线程将失去处理器
    try{
      Thread.sleep(100);
    } catch (Exception e){
      System.exit(0);
    }

     // 显示新的计数器值
    txtGénérés.setText("");
    txtGénérés.setText(""+compteurs[iThread]);
    // 后续
    System.out.println("Thread " + iThread + " : " + compteurs[iThread]);
  }// 递增

}// 归类

详细说明代码:

  • 该窗口声明了两个实例变量:
   // 实例变量
    Thread[] tâches=null;   // 线程
    int[] compteurs=null;   // 计数器

数组 tâches 将作为生成的线程数组。数组 compteurs 将与数组 tâches 关联。 每个任务都将拥有一个专属计数器,用于从图形界面中获取字段 txtGénérés 的值。

  • 点击按钮 Générer 时,将执行方法 btnGénérer_actionPerformed
  • 该方法首先获取要生成的线程数量。如果该数量不可用,则会报告错误。随后,它生成所需的线程,同时将它们的引用记录在一个数组中,并为每个线程分配一个编号。 生成的线程调用方法 run,该方法会调用类中的方法 incrémente。所有线程均被启动(start)。同时,还创建了与线程关联的计数器数组。
  • 方法 incrémente
  • 读取字段 txtGénérés 的当前值,并将其存储在当前正在运行的线程所属的计数器中
  • 暂停 100 毫秒,以此有意让出处理器
  • 将新值显示在字段 txtGénérés

现在我们来解释一下为什么线程计数不正确。假设需要生成 2 个线程。它们以不可预测的顺序执行。其中一个先执行,并读取计数器的值 0。 随后它将计数器值改为1,但并未将其写入窗口:它主动暂停100毫秒。此时它失去了处理器控制权,处理器被分配给另一个线程。该线程的操作方式与前一个相同:它读取窗口计数器,并获取其中仍存在的0。 它将计数器值改为1,随后像前一个线程一样暂停100毫秒。此时处理器再次分配给第一个线程:该线程将值1写入窗口计数器并结束运行。处理器现在分配给第二个线程,它也会写入1。结果是不正确的。

问题出在哪里?第二个线程读取了错误的值,因为第一个线程在完成更新窗口计数器的任务之前就被中断了。这引出了程序中关键资源和关键区段的概念:

  • 关键资源是指一次只能由一个线程持有的资源。在此,关键资源是窗口中的计数器2。
  • 程序中的关键段是指线程执行流中的一段指令序列,在此期间线程会访问关键资源。必须确保在此关键段内,该线程是唯一能够访问该资源的线程。

7.6.2. 通过方法实现的计数同步

在上一个示例中,每个线程都执行了窗口的 incrémente 方法。incrémente 方法的声明如下:

    private void incremente()

现在我们将其声明为:

   // 递增
  private synchronized void incrémente(){

关键字 synchronized 表示每次只有一个线程可以执行方法 incrémente。请考虑以下符号:

  • 窗口对象 F btnGénérer_actionPerformed 中创建线程
  • 由 F 创建的两个线程 T1 和 T2

这两个线程由 F 创建并启动。因此,它们都将执行方法 F.run。 假设 T1 先到达。它执行 F.run,然后执行 F.incremente(这是一个同步方法)。 它读取计数器的值 0,将其递增,然后暂停 100 毫秒。此时处理器被分配给 T2,该线程依次执行 F.run 以及 F.incremente。 此时它被阻塞了,因为线程 T1 正在执行 F.incremente,而关键字 synchronized 确保每次只有一个线程可以执行 F.incremente。 T2 随后也失去了处理器控制权,未能读取计数器的值。 100 毫秒后,T1 重新获得处理器,显示计数器值为 1,退出 F.incremente 然后 F.run,并终止。 此时 T2 重新获得处理器控制权,并能执行 F.incremente,因为 T1 已不再执行该方法。 随后,T2读取计数器的值1,将其递增,并暂停100毫秒。100毫秒后,它重新获得处理器控制权,显示计数器的值2,并同样结束运行。这次,得到的值是正确的。以下是一个经过测试的示例:

Image

7.6.3. 由对象同步的计数

在上一个示例中,对计数器 txtGénérés 的访问通过一个方法进行了同步。如果创建线程的窗口名为 F,那么也可以说方法 F.incremente 代表了一项资源,该资源每次只能由一个线程使用。因此,这是一项关键资源。 对该资源的同步访问由关键字 synchronized 保证:

    private synchronized void incrémente()

也可以说,关键资源就是对象 F 本身。这比关键资源是 F.incremente 的情况更为严格。事实上,在后一种情况下,如果线程 T1 执行 F.incremente, 则线程 T2 虽然无法执行 F.incremente,但可以执行对象 F 的其他方法,无论这些方法是否被同步。 如果对象 F 本身就是关键资源,当线程 T1 执行该对象的某个同步段时,该对象的其他所有同步段对其他线程都将不可访问。 因此,如果线程 T1 执行了同步方法 F.incremente,则线程 T2 不仅无法执行 F.incremente,也无法执行 F 的任何其他同步段, 即使没有任何线程正在使用该部分。因此,这是一种限制更严格的方法。

假设窗口成为关键资源。那么我们将编写:

   // 递增
  private void incrémente(){
     // 临界区
    synchronized(this){
       // 获取线程编号
      int iThread=0;
      try{
        iThread=Integer.parseInt(Thread.currentThread().getName());
      }catch(Exception ex){}
       // 读取任务计数器的值
      try{
        compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
      } catch (Exception e){}
       // 将其递增
      compteurs[iThread]++;

       // 等待 100 毫秒——此时线程将失去处理器
      try{
        Thread.sleep(100);
      } catch (Exception e){
        System.exit(0);
      }

       // 显示新的计数器值
      txtGénérés.setText("");
      txtGénérés.setText(""+compteurs[iThread]);
    }//同步
  }// 递增

所有线程都使用窗口 this 进行同步。运行时,结果与之前一样正确。实际上,我们可以使用任何所有线程都已知的对象进行同步。例如,以下是一种能得到相同结果的另一种方法:

   // 实例变量
    Thread[] tâches=null;   // 线程
    int[] compteurs=null;   // 计数器
    Object synchro=new Object(); // 线程同步对象

   // 递增
  private void incrémente(){
     // 关键部分
    synchronized(synchro){
..............
    }//同步
  }// 递增

该窗口创建了一个类型为 Object 的对象,用于线程同步。这种方法比基于 this 对象进行同步的方法更优,因为限制更少。 在此情况下,如果一个线程 T1 位于 incrémente 的同步段内,而另一个线程 T2 想要执行同一 this 对象的另一个同步段(该段由 synchro 以外的对象进行同步),它也能成功执行。

7.6.4. 基于事件的同步

这次,我们使用一个布尔变量 peutPasser 来指示线程是否可以进入临界区。不使用同步的写法可能如下:

while(! peutPasser);        // 等待 peutPasser 变为真
peutPasser=false;            // 其他线程不得通过
section critique;            // 此时该线程处于独占状态
peutPasser=true;            // 另一个线程可以进入关键部分

第一个语句中,线程在循环等待 peutPasser 变为真,这种做法不够优雅:线程会无谓地占用处理器资源。这被称为“活跃等待”。我们可以将代码优化如下:

while(! peutPasser){        // 等待 peutPasser 变为真
   Thread.sleep(100);    // 暂停 100 毫秒
}
peutPasser=false;            // 不应有其他线程通过
section critique;            // 此时该线程处于独占状态
peutPasser=true;            // 另一个线程可以进入临界区

这里的等待循环更优:如果线程无法进入,它会休眠100毫秒,然后再次检查是否可以进入。在此期间,处理器将被分配给系统中的其他线程。

这两种方法实际上都是错误的:它们无法防止两个线程同时闯入临界区。假设一个线程 T1 检测到 peutPasser 为真。 它将执行下一条指令,将 peutPasser 设为假以阻塞其他线程。然而,此时它很可能被中断,无论是因为其处理器时间片用尽,还是因为优先级更高的任务请求了处理器,抑或其他原因。 结果是它失去了处理器控制权。稍后它将重新获得控制权。在此期间,其他任务将获得处理器控制权,其中可能包括一个名为 T2 的线程,该线程正处于循环状态,等待 peutPasser 变为真。 它也会发现 peutPasser 处于真状态(第一个线程来不及将其设为假),并随之进入临界区。这正是我们不希望发生的情况。

该序列


while(! peutPasser){            // 等待 peutPasser 变为真
   try{
        Thread.sleep(100);    // 暂停 100 毫秒
    } catch (Exception e) {}
}// while
peutPasser=false;                // 不允许其他线程通过

是一段关键代码,必须通过同步机制加以保护。参照前面的示例,我们可以编写如下代码:


    synchronized(synchro){
        while(! peutPasser){            // 等待 peutPasser 变为真
            try{
                Thread.sleep(100);    // 暂停 100 毫秒
            } catch (Exception e) {}
        }//while
        peutPasser=false;                // 其他线程不得通过
    }// synchronized
    section critique;                    // 此处线程处于独占状态
    peutPasser=true;                    // 另一个线程可以进入临界区

此示例运行正常。可以通过避免线程在定期监视布尔值 peutPasser 时处于半活动状态来对其进行优化。 与其每 100 毫秒定期唤醒一次来检查 peutPasser 的状态,不如让它进入休眠状态,并要求在 peutPasser 为真时将其唤醒。实现方式如下:


synchronized(synchro){
    if (! peutPasser) {
        try{
            synchro.wait();            // 如果无法进入,则等待
        } catch (Exception e){
            
        }
    }
    peutPasser=false;            // 其他线程均不得进入
}// synchronized

操作 synchro.wait() 只能由 synchro 对象的当前“拥有者”线程执行。此处的序列为:


synchronized(synchro){

}// synchronized

该序列确保该线程拥有对象 synchro。通过调用 synchro.wait() 操作,该线程将同步锁的所有权释放。为什么这样做?通常是因为它缺少继续工作的资源。 因此,与其阻塞其他正在等待资源 synchro 的线程,它宁愿释放该资源,并转而等待自己缺失的资源。在本例中,它等待布尔值 peutPasser 变为 true。它将如何获知此事件?方式如下:


synchronized(synchro){
    if (! peutPasser) {
        try{
            synchro.wait();            // 如果无法进入,则等待
        } catch (Exception e){
            
        }
    }
    peutPasser=false;            // 其他线程不得通过
}// synchronized
section critique...
synchronized(synchro){
      synchro.notify();
    }

考虑第一个通过同步锁的线程。将其命名为 T1。 假设它发现布尔值 peutPasser 为真,因为它是第一个进入的。因此,它将其设为假。随后,它退出由对象 synchro 锁定的临界区。 此时,另一个线程可以进入临界区来检查 peutPasser 的值。它会发现该值为假,于是开始等待一个事件(wait)。在此过程中,它将 synchro 对象的所有权让渡出去。 此时另一个线程可以进入临界区:它也会进入等待状态,因为 peutPasser 的状态为假。因此,可能会有多个线程在等待 synchro 对象上的事件。

让我们回到已获得该对象的线程 T1。它执行了关键部分,随后会指示另一个线程现在可以接手。它通过以下序列实现:


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

它首先必须通过指令 synchronized 重新获取对象 synchro。 这应该不会造成问题,因为它与那些线程处于竞争状态——即使这些线程暂时获得了对象 synchro,也必须通过 wait 将其释放,因为它们会发现 peutPasser 为假。 因此,我们的线程 T1 最终将成功获得对象 synchro 的所有权。 完成此操作后,它通过 synchro.notify 操作指示,被 synchro.wait 阻塞的线程之一必须被唤醒。随后,它再次放弃对 synchro 对象的所有权,该对象随后将被分配给其中一个等待中的线程。 该线程将从导致其挂起的 wait 指令之后的下一条指令继续执行。随后,它将执行关键区,并执行 synchro.notify 来释放另一个线程。如此循环往复。

让我们通过之前研究过的计数示例来了解这种工作模式。

  void btnGénérer_actionPerformed(ActionEvent e) {
     //生成线程

     // 读取要生成的线程数
    int nbThreads=0;
    try{
      // 读取包含线程数量的字段
      nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
      // 正数 >
      if(nbThreads<=0) throw new Exception();
    }catch(Exception ex){
       //错误
      txtStatus.setText("Nombre invalide");
       // 重新开始
      txtAGénérer.requestFocus();
      return;
    }//捕获

     // RAZ 任务计数器
    txtGénérés.setText("0");  // 任务计数器为 0
    // 第一个线程可以运行
    peutPasser=true;
     // 生成并启动线程
    tâches=new Thread[nbThreads];
    compteurs=new int[nbThreads];
    for(int i=0;i<tâches.length;i++){
      // 创建线程 i
      tâches[i]=new Thread() {
          public void run() {
          synchronise();
        }
      };//线程 i
       // 定义其名称
      tâches[i].setName(""+i);
      // 启动其执行
      tâches[i].start();
    }//for
  }//生成

现在,线程不再执行方法 incrémente,而是执行以下方法 synchronise

   // 线程同步阶段
  public void synchronise(){
     // 请求访问临界区
    synchronized(synchro){
      try{
        // 能否进入?
        if(! peutPasser){
          System.out.println(Thread.currentThread().getName()+ " en attente");
          synchro.wait();
        }
         // 已通过——禁止其他线程通过
        peutPasser=false;
      } catch(Exception e){
        txtStatus.setText(""+e);
                    return;
      }//try
    }// synchronized

    // 临界区
    System.out.println(Thread.currentThread().getName()+ " passé");
    incrémente();

     // 已经完成——释放可能在关键区入口处被阻塞的线程
    peutPasser=true;
    System.out.println(Thread.currentThread().getName()+ " terminé");
    synchronized(synchro){
      synchro.notify();
    }// synchronized
  } // 同步

方法 synchronise 的目的是让线程依次处理。为此,它使用了一个同步变量 synchro。 方法 incrémente 现已不再受关键字 synchronized 的保护:

   // 递增
  private void incrémente(){
     // 获取线程编号
    int iThread=0;
    try{
      iThread=Integer.parseInt(Thread.currentThread().getName());
    }catch(Exception ex){}
     // 读取任务计数器的值
    try{
      compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
    } catch (Exception e){}
     // 递增计数器
    compteurs[iThread]++;
     // 等待 100 毫秒——此时线程将失去处理器
    try{
      Thread.sleep(100);
    } catch (Exception e){
      System.exit(0);
    }
     // 显示新的计数器值
    txtGénérés.setText("");
    txtGénérés.setText(""+compteurs[iThread]);
  }// 递增

对于 5 个线程,所得结果如下:

0 passé
1 en attente
2 en attente
3 en attente
4 en attente
0 terminé
1 passé
1 terminé
2 passé
2 terminé
3 passé
3 terminé
4 passé
4 terminé

假设 T0 至 T4 是应用程序生成的 5 个线程。 T0 首先获取锁 synchro 的所有权,并发现 peutPasser 为真。 它将 peutPasser 设为 false 并继续执行:这就是第一个消息 passé 的含义。 极有可能,它会继续执行关键部分,特别是 incrémente 方法。在此方法中,它将休眠 100 毫秒(sleep)。 因此它释放了处理器。处理器被分配给另一个线程——T1线程,该线程随即获得了对象synchro的所有权。 该线程发现无法继续执行,于是进入等待状态(wait)。随后,它释放了对对象 synchro 的持有权以及处理器。处理器被分配给线程 T2,该线程也遭遇了同样的命运。 在 T0 暂停的 100 毫秒内,线程 T1 至 T4 因此被挂起。这就是 4 “挂起”消息的含义。 100毫秒后,T0重新获得处理器并完成其任务:这就是“0 terminé”消息的含义。随后,它释放其中一个被阻塞的线程并终止。 释放的处理器资源随后被分配给一个空闲线程:即刚刚被释放的那个。此处是 T1。随后,线程 T1 进入临界区:这就是消息“1 passé”的含义。 它完成应做的工作后,也将暂停 100 毫秒。此时处理器可供其他线程使用,但所有线程都在等待某个事件:因此没有一个线程能获得处理器。 100 毫秒后,线程 T1 重新获得处理器并结束运行:这就是消息“1 terminé”的含义。 线程 T1 至 T4 将表现出与 T1 相同的行为:这就是三组消息“已通过”、“已结束”的含义。