线程的生命周期和状态控制

简介: 线程的生命周期和状态控制

一、线程的生命周期

线程的生命周期,就是一个线程从创建到消亡的过程。关于Java中线程的生命周期,首先看一下下面这张较为经典的图:

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种不同的状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

新建状态(New)

创建Thread类的实例成功后,则该线程对象就处于新建状态。处于新建状态的线程有自己的内存空间,通过调用start()方法进入就绪状态(Runnable)。

就绪状态(Runnable)

处于就绪状态的线程已经具备了运行条件(也就是具备了在CPU上运行的资格),但还没有分配到CPU的执行权,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用run()方法。

运行状态(Running)

处于就绪状态的线程,如果获得了CPU的调度,就会从就绪状态变为运行状态,执行run()方中的任务。

运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

如果该线程失去了CPU资源,就会又从运行状态变为就绪状态,重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出CPU资源,再次变为就绪状态。

阻塞状态(Blocked)

在某种特殊的情况下,被人挂起或执行输入输出操作时,让出CPU执行权并临时中断自己的执行,从而进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。

根据阻塞产生的原因不同,阻塞状态又可以分为三种:

  1. 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态。当调用notify()或notifyAll()等方法,则该线程就会重新转入就绪状态。
  2. 同步阻塞:线程在获取同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。当获取同步锁成功,则该线程就会重新转入就绪状态。
  3. 其它阻塞:通过调用线程sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,则该线程就会重新转入就绪状态。

死亡状态(Dead)

线程在run()方法执行完了或者因异常退出了run()方法,该线程结束生命周期。此外,如果线程执行了interrupt()或stop()方法,那么它也会以异常退出的方式进入死亡状态。

二、线程状态的控制

Java提供了一些便捷的方法用于会线程状态的控制。线程状态的控制Java给我们提供了很多方法,但是有些已经标注为过时的,我们应该尽可能的避免使用它们,此处我们重点关注start()、join()、sleep()、yield()等直接控制方法,和setDaemon()、setPriority()等间接控制方法。

线程睡眠

如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,指定时间之后,解除阻塞状态,进入就绪状态,则可以通过调用Thread的sleep()方法来实现。从API查阅可以看到sleep()方法有两种重载的形式,但是使用方式一模一样。

比如,我们想要使主线程每休眠1000毫秒,然后再打印出数字:

【示例】每隔1000毫秒打印一个数字

public class Test {
  public static void main(String[] args) {
    for(int i = 1; i < 10; i++) {
      System.out.println(i);
      try {
        Thread.sleep(1000); // 对主线程休眠1秒钟
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

通过执行代码,我们可以明显看到打印的数字在时间上有些许的间隔。

注意事项:

  1. sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。
  2. 使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。

线程的优先级

每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。

Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,能够设置[1-10]之间的整数,数值越大,那么优先级越高,也可以使用Thread类提供的三个静态常量:

4. public final static int MIN_PRIORITY = 1;

5. public final static int NORM_PRIORITY = 5; 默认

6. public final static int MAX_PRIORITY = 10;

【示例】测试线程优先级的执行

/**
 * 自定义线程类
 */
class TestThread extends Thread {
  public TestThread() {}
  public TestThread(String name, int pro) {
    super(name); // 设置线程名字
    this.setPriority(pro); // 设置线程的优先级
  }
  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      System.out.println(this.getName() + "线程第" + i + "次执行!");
    }
  }
}
/**
 * 测试类
 */
public class Test {
  public static void main(String[] args) {
    new TestThread("高级", 10).start();
    new TestThread("低级", 1).start();
  }
}

执行程序,从结果可以看到,一般情况下,高级线程更先执行完毕。

线程让步

yield()方法和sleep()方法有点相似,yield()方法也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出CPU资源给其它的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。

实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉CPU调度线程。

【示例】线程让步的使用
/**
 * 自定义线程类
 */
class TestThread extends Thread {
  public TestThread() {}
  public TestThread(String name, int pro) {
    super(name); // 设置线程名字
    this.setPriority(pro); // 设置线程的优先级
  }
  @Override
  public void run() {
    for (int i = 0; i < 5; i++) {
      System.out.println(this.getName() + "线程第" + i + "次执行!");
      Thread.yield(); // 线程让步
    }
  }
}
/**
 * 测试类
 */
public class Test {
  public static void main(String[] args) {
    new TestThread("高级", 10).start();
    new TestThread("低级", 1).start();
  }
}

关于sleep()方法和yield()方的区别如下:

sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。
 sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。
 sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行,因此开发中yield方法不常用。

线程合并

线程的合并就是:线程A在运行期间,可以调用线程B的join()方法,这样线程A就必须等待线程B执行完毕后,才能继续执行A线程。

应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。

线程合并有三个重载的方法:

【示例】线程合并的使用

/**
 * 自定义线程类
 */
class TestThread extends Thread {
  public TestThread() {}
  public TestThread(String name, int pro) {
    super(name); // 设置线程名字
    this.setPriority(pro); // 设置线程的优先级
  }
  @Override
  public void run() {
    for (int i = 0; i < 30; i++) {
      System.out.println(this.getName() + "线程第" + i + "次执行!");
    }
  }
}
/**
 * 测试类
 */
public class Test {
  public static void main(String[] args) {
    TestThread th = new TestThread();
    th.start();
    try {
      // 等待th线程执行任务完毕之后,再执行主线程中的任务
      th.join();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
        // 主线程任务
    for (int i = 0; i < 30; i++) {
      String name = Thread.currentThread().getName();
      System.out.println(name + "线程第" + i + "次执行!");
    }
  }
}

在这个例子中,在主线程中调用th.join(); 就是将主线程加入到th子线程后面等待执行。

守护线程

守护线程与普通线程写法上基本没啥区别,调用线程对象的方法setDaemon(true),就可以把该线程标记为守护线程。

当普通线程(前台线程)都全部执行完毕,也就是当前在运行的线程都是守护线程时,Java虚拟机(JVM)将退出。另外,setDaemon(true)方法必须在启动线程前调用,否则抛出IllegalThreadStateException异常。

【示例】线程合并的使用

// 前台线程
class CommonThread extends Thread {
  @Override
  public void run() {
    for (int i = 0; i < 5; i++) {
      System.out.println("前台线程第" + i + "次执行!");
    }
  }
}
// 后台线程或守护线程
class DaemonThread extends Thread {
  int i = 0;
  @Override
  public void run() {
    while(true) {
      System.out.println("后台线程第" + i++ + "次执行!");
    }
  }
}
//测试类
public class Test {
  public static void main(String[] args) {
    CommonThread ct = new CommonThread();
    DaemonThread dt = new DaemonThread();
    ct.start();
    dt.setDaemon(true); // 将dt设置为守护线程
    dt.start();
  }
}

运行以上代码,通过执行的结果可以看出:前台线程是保证执行完毕的,后台线程还没有执行完毕就退出了。

守护线程的用途:守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。

相关文章
|
5月前
|
Java
【编程侦探社】追踪 Java 线程:一场关于生命周期的侦探故事!
【6月更文挑战第19天】在Java世界中,线程如同神秘角色,编程侦探揭示其生命周期:从新生(`new Thread()`)到就绪(`start()`),面临并发挑战如资源共享冲突。通过`synchronized`实现同步,处理阻塞状态(如等待锁`synchronized (lock) {...}`),最终至死亡,侦探深入理解并解决了多线程谜题,成为编程侦探社的传奇案例。
34 1
|
16天前
|
Java API 调度
Java 线程的生命周期
在JDK 1.5之前,线程的生命周期包括五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。JDK 1.5及之后增加了三种阻塞状态,共六种状态:新建、可运行、终止、锁阻塞、计时等待和无限等待。这些状态描述了线程在操作系统和JVM中的不同阶段。
Java 线程的生命周期
|
20天前
|
Java 调度
[Java]线程生命周期与线程通信
本文详细探讨了线程生命周期与线程通信。文章首先分析了线程的五个基本状态及其转换过程,结合JDK1.8版本的特点进行了深入讲解。接着,通过多个实例介绍了线程通信的几种实现方式,包括使用`volatile`关键字、`Object`类的`wait()`和`notify()`方法、`CountDownLatch`、`ReentrantLock`结合`Condition`以及`LockSupport`等工具。全文旨在帮助读者理解线程管理的核心概念和技术细节。
33 1
[Java]线程生命周期与线程通信
|
5月前
|
Java API 调度
深入解析Java线程状态与生命周期
深入解析Java线程状态与生命周期
35 1
|
1月前
|
Java 调度
Java一个线程的生命周期详解
Java中,一个线程的生命周期分为五个阶段:NEW(新建),RUNNABLE(可运行),BLOCKED(阻塞),WAITING(等待),TERMINATED(终止)。线程创建后处于新建状态,调用start方法进入可运行状态,执行中可能因等待资源进入阻塞或等待状态,正常完成或异常终止后进入终止状态。各状态间可相互转换,构成线程的生命周期。
|
3月前
|
Java 调度
【多线程面试题 五】、 介绍一下线程的生命周期
线程的生命周期包括新建、就绪、运行、阻塞和死亡状态,线程状态会根据线程的执行情况在这些状态之间转换。
【多线程面试题 五】、 介绍一下线程的生命周期
|
3月前
|
安全 Java 调度
线程的状态和生命周期
在多线程编程中,线程的状态和生命周期是两个非常重要的概念。了解线程的状态和生命周期可以帮助我们更好地理解和编写多线程程序。
57 4
|
5月前
|
Java
【技术瑜伽师】Java 线程:修炼生命周期的平衡之道,达到多线程编程的最高境界!
【6月更文挑战第19天】Java多线程编程犹如瑜伽修行,从创建线程开始,如`new Thread(Runnable)`,到启动线程的活跃,用`start()`赋予生命。面对竞争与冲突,借助同步机制保证资源访问的有序,如`synchronized`关键字。线程可能阻塞等待,如同瑜伽的静止与耐心。完成任务后线程终止,整个过程需密切关注状态变换,以求多线程间的和谐与平衡。持续修炼,如同瑜伽般持之以恒,实现高效稳定的多线程程序。
28 3
|
5月前
|
Java
【代码诗人】Java线程的生与死:一首关于生命周期的赞歌!
【6月更文挑战第19天】Java线程生命周期,如诗般描绘了从新建到死亡的旅程:创建后待命,`start()`使其就绪,获得CPU则运行,等待资源则阻塞,任务完或中断即死亡。理解生命周期,善用锁、线程池,优雅处理异常,确保程序高效稳定。线程管理,既是艺术,也是技术。
30 3
|
5月前
|
安全 Java
【极客档案】Java 线程:解锁生命周期的秘密,成为多线程世界的主宰者!
【6月更文挑战第19天】Java多线程编程中,掌握线程生命周期是关键。创建线程可通过继承`Thread`或实现`Runnable`,调用`start()`使线程进入就绪状态。利用`synchronized`保证线程安全,处理阻塞状态,注意资源管理,如使用线程池优化。通过实践与总结,成为多线程编程的专家。
46 3