返回介绍

4.2 启动和终止线程

发布于 2024-08-18 10:09:30 字数 6889 浏览 0 评论 0 收藏 0

在前面章节的示例中通过调用线程的start()方法进行启动,随着run()方法的执行完毕,线程也随之终止,大家对此一定不会陌生,下面将详细介绍线程的启动和终止。

4.2.1 构造线程

在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。代码清单4-6所示的代码摘自java.lang.Thread中对线程进行初始化的部分。

代码清单4-6 Thread.java

private void init(ThreadGroup g, Runnable target, String name,long stackSize, 
AccessControlContext acc) {
      if (name == null) {
       throw new NullPointerException("name cannot be null");
      }
      // 当前线程就是该线程的父线程
      Thread parent = currentThread();
      this.group = g;
      // 将daemon、priority属性设置为父线程的对应属性
      this.daemon = parent.isDaemon();
      this.priority = parent.getPriority();
      this.name = name.toCharArray();
      this.target = target;
      setPriority(priority);
      // 将父线程的InheritableThreadLocal复制过来
      if (parent.inheritableThreadLocals != null) 
      this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.
      inheritableThreadLocals);
      // 分配一个线程ID
      tid = nextThreadID();
}

在上述过程中,一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。至此,一个能够运行的线程对象就初始化好了,在堆内存中等待着运行。

4.2.2 启动线程

线程对象在初始化完成之后,调用start()方法就可以启动这个线程。线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。

注意 启动一个线程前,最好为这个线程设置线程名称,因为这样在使用jstack分析程序或者进行问题排查时,就会给开发人员提供一些提示,自定义的线程最好能够起个名字。

4.2.3 理解中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。

从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。

在代码清单4-7所示的例子中,首先创建了两个线程,SleepThread和BusyThread,前者不停地睡眠,后者一直运行,然后对这两个线程分别进行中断操作,观察二者的中断标识位。

代码清单4-7 Interrupted.java

public class Interrupted {
    public static void main(String[] args) throws Exception {
  // sleepThread不停的尝试睡眠
  Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
  sleepThread.setDaemon(true);
  // busyThread不停的运行
  Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
  busyThread.setDaemon(true);
  sleepThread.start();
  busyThread.start();
  // 休眠5秒,让sleepThread和busyThread充分运行
  TimeUnit.SECONDS.sleep(5);
  sleepThread.interrupt();
  busyThread.interrupt();
  System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
  System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
  // 防止sleepThread和busyThread立刻退出
  SleepUtils.second(2);
    }
    static class SleepRunner implements Runnable {
  @Override
  public void run() {
      while (true) {
    SleepUtils.second(10); 
      }
  }
    }
    static class BusyRunner implements Runnable {
  @Override
  public void run() {
      while (true) {
      }
  }
    }
}

输出如下。

SleepThread interrupted is false
BusyThread interrupted is true

从结果可以看出,抛出InterruptedException的线程SleepThread,其中断标识位被清除了,而一直忙碌运作的线程BusyThread,中断标识位没有被清除。

4.2.4 过期的suspend()、resume()和stop()

大家对于CD机肯定不会陌生,如果把它播放音乐比作一个线程的运作,那么对音乐播放做出的暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。

在代码清单4-8所示的例子中,创建了一个线程PrintThread,它以1秒的频率进行打印,而主线程对其进行暂停、恢复和停止操作。

代码清单4-8 Deprecated.java

public class Deprecated {
    public static void main(String[] args) throws Exception {
  DateFormat format = new SimpleDateFormat("HH:mm:ss");
  Thread printThread = new Thread(new Runner(), "PrintThread");
  printThread.setDaemon(true);
  printThread.start();
  TimeUnit.SECONDS.sleep(3);
  // 将PrintThread进行暂停,输出内容工作停止
  printThread.suspend();
  System.out.println("main suspend PrintThread at " + format.format(new Date()));
  TimeUnit.SECONDS.sleep(3);
  // 将PrintThread进行恢复,输出内容继续
  printThread.resume();
  System.out.println("main resume PrintThread at " + format.format(new Date()));
  TimeUnit.SECONDS.sleep(3);
  // 将PrintThread进行终止,输出内容停止
  printThread.stop();
  System.out.println("main stop PrintThread at " + format.format(new Date()));
  TimeUnit.SECONDS.sleep(3);
    }
    static class Runner implements Runnable {
  @Override
  public void run() {
      DateFormat format = new SimpleDateFormat("HH:mm:ss");
      while (true) {
    System.out.println(Thread.currentThread().getName() + " Run at " + 
        format.format(new Date()));
    SleepUtils.second(1);
      }
  }
    }
}

输出如下(输出内容中的时间与示例执行的具体时间相关)。

PrintThread Run at 17:34:36
PrintThread Run at 17:34:37
PrintThread Run at 17:34:38
main suspend PrintThread at 17:34:39
main resume PrintThread at 17:34:42
PrintThread Run at 17:34:42
PrintThread Run at 17:34:43
PrintThread Run at 17:34:44
main stop PrintThread at 17:34:45

在执行过程中,PrintThread运行了3秒,随后被暂停,3秒后恢复,最后经过3秒被终止。

通过示例的输出可以看到,suspend()、resume()和stop()方法完成了线程的暂停、恢复和终止工作,而且非常“人性化”。但是这些API是过期的,也就是不建议使用的。

不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。

注意 正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法,而暂停和恢复操作可以用后面提到的等待/通知机制来替代。

4.2.5 安全地终止线程

在4.2.3节中提到的中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个boolean变量来控制是否需要停止任务并终止该线程。

在代码清单4-9所示的例子中,创建了一个线程CountThread,它不断地进行变量累加,而主线程尝试对其进行中断操作和停止操作。

代码清单4-9 Shutdown.java

public class Shutdown {
    public static void main(String[] args) throws Exception {
  Runner one = new Runner();
  Thread countThread = new Thread(one, "CountThread");
  countThread.start();
  // 睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束
  TimeUnit.SECONDS.sleep(1);
  countThread.interrupt();
  Runner two = new Runner();
  countThread = new Thread(two, "CountThread");
  countThread.start();
  // 睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为false而结束
  TimeUnit.SECONDS.sleep(1);
  two.cancel();
    }
    private static class Runner implements Runnable {
  private long i;
      private volatile boolean on = true;
      @Override
      public void run() {
      while (on && !Thread.currentThread().isInterrupted()){
    i++;
      }
      System.out.println("Count i = " + i);
  }
  public void cancel() {
      on = false;
  }
    }
}

输出结果如下所示(输出内容可能不同)。

Count i = 543487324
Count i = 540898082

示例在执行过程中,main线程通过中断操作和cancel()方法均可使CountThread得以终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。

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

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

发布评论

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