深入浅出Java多线程(四):线程状态

引言


大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第四篇内容:线程状态。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!

在现代软件开发中,多线程编程已经成为提升应用程序性能和响应能力的关键技术。Java作为一门支持多线程编程的主流语言,其内置的丰富并发库使得开发者能够轻松创建、管理和协调多个线程以实现高效的并发执行。然而,深入理解和掌握Java线程的工作机制及其状态变化规律,是编写出稳定、高效并发程序的前提。

在Java中,一个线程在其生命周期内会经历一系列的状态变迁,从刚刚创建但尚未启动的新建状态(NEW),到正在运行或等待CPU时间片的就绪/运行状态(RUNNABLE),再到因争夺锁资源而暂时阻塞的BLOCKED状态,以及因调用等待方法进入等待其他线程唤醒的WAITING或TIMED_WAITING状态,直至线程执行完毕后的终止状态(TERMINATED)。这些状态的准确转换与管理对于理解线程的行为至关重要,也是排查诸如死锁、饥饿等问题的根本依据。

例如,在一个多线程环境下,当一个线程尝试获取已被其他线程持有的锁时,它将由RUNNABLE状态转变为BLOCKED状态,如以下代码片段所示:

synchronized (lock) {
    // 进入同步代码块前需获得锁,否则线程t2将会被阻塞
    Thread t1 = new Thread(() -> {
        // 持有锁并执行操作
    });
    t1.start();

    Thread t2 = new Thread(() -> {
        synchronized (lock) { // 线程t2试图获取已被t1持有的锁,因此变为BLOCKED状态
            // 执行相关操作
        }
    });
    t2.start();
}

同时,Java还提供了中断机制,通过Thread.interrupt()方法可以设置线程的中断标志位,而非直接强制停止线程,这就需要程序员在设计线程任务时关注如何正确响应中断请求,确保程序能在需要时优雅地关闭线程,如下所示:

Thread thread = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // 业务处理逻辑...
    }
    // 当线程收到中断信号后退出循环,并进行清理工作
});
thread.start();
// 在某个时刻决定中断线程
thread.interrupt();

因此,本篇博客将深入剖析Java线程的各种状态及其转化过程,结合具体的生活场景及代码示例,帮助读者建立起对Java线程状态全面而直观的认识,从而更好地驾驭多线程编程,提高并发程序的质量与可维护性。

操作系统线程状态


在现代操作系统中,线程被视为轻量级进程,它们的状态与进程状态有着紧密的对应关系。操作系统中的线程主要有三个基本状态:就绪状态、执行状态和等待状态。

  1. 就绪状态(ready) 在这个状态下,线程已经准备就绪,具备了运行条件,等待操作系统的CPU调度器为其分配处理器资源。一旦获得CPU时间片,线程便能立即进入执行状态。在Java虚拟机(JVM)内部,这一状态被合并到RUNNABLE状态中,意味着一个Java线程在JVM层面可能是就绪态或者正在执行。

  2. 执行状态(running) 当线程获得CPU并开始执行其任务时,它处于执行状态。在这个状态下,线程会占用CPU进行计算、读写内存等操作。对于Java线程而言,其RUNNABLE状态同样包括了线程实际在执行的过程。

  3. 等待状态(waiting) 线程由于等待特定事件的发生或等待系统资源(如I/O完成)而暂时放弃CPU使用权,进入等待状态。例如,在Java中调用Object.wait()方法后,线程将释放持有的锁并进入WAITING状态,直到其他线程通过notify()或notifyAll()将其唤醒:

    synchronized (obj) {
        obj.wait(); // 线程在此处进入等待状态
    }

    另外,当线程因无法获取所需资源(如互斥锁)而暂停执行时,它也会进入BLOCKED状态,这在多线程同步场景中很常见:

    synchronized (lock) {
        // 若lock已被其他线程持有,则新尝试获取该锁的线程会进入BLOCKED状态
    }

综合来看,操作系统线程的状态转换是动态且频繁发生的,由操作系统内核的调度策略决定。而在Java编程中,虽然直接映射的是Java线程的六种状态,但其背后仍遵循操作系统线程的基本状态转换逻辑,并为开发者提供了更为细致的控制手段来管理线程生命周期。

Java线程的六种状态详解


  1. NEW状态 当创建一个Thread对象但尚未调用其start()方法时,线程处于NEW状态。在这个状态下,线程并未启动,仅完成了初始化阶段。例如:

    Thread thread = new Thread(() -> {
        // 任务代码
    });
    System.out.println(thread.getState()); // 输出 NEW

    一旦调用了start()方法,线程的状态将发生改变,开始执行线程体内的代码。值得注意的是,同一个线程不能重复调用start()方法,否则会抛出IllegalThreadStateException异常。

  2. RUNNABLE状态 RUNNABLE是Java中较为特殊的一个状态,它涵盖了传统操作系统中的就绪和运行两种状态。当线程已启动且CPU调度器为其分配了时间片或线程正在等待系统资源(如I/O操作)时,线程都处于RUNNABLE状态。在Java虚拟机(JVM)中,这样的线程既可能实际在执行,也可能随时准备执行。

  3. BLOCKED状态 BLOCKED状态表示线程因尝试获取锁而被阻塞,暂时无法继续执行。以下是一个模拟线程争夺锁从而进入BLOCKED状态的例子:

    Object lock = new Object();

    Thread t1 = new Thread(() -> {
        synchronized (lock) {
            try {
                Thread.sleep(1000); // 持有锁并休眠
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    Thread t2 = new Thread(() -> {
        synchronized (lock) { // 尝试获取已被t1持有的锁,因此进入BLOCKED状态
            // 执行相关操作
        }
    });

    t1.start();
    t2.start();

  4. WAITING状态 当线程调用Object.wait()、Thread.join()或者LockSupport.park()等方法后,主动放弃当前持有的锁并进入WAITING状态,此时线程必须由其他线程通过notify()、notifyAll()或LockSupport.unpark()方法唤醒才能恢复到RUNNABLE状态。 举例来说,假设两个线程间的同步与唤醒过程如下:

    Object monitor = new Object();

    Thread waiter = new Thread(() -> {
        synchronized (monitor) {
            try {
                monitor.wait(); // 线程进入WAITING状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    Thread notifier = new Thread(() -> {
        synchronized (monitor) {
            // 做一些操作...
            monitor.notify(); // 唤醒waiter线程
        }
    });

    waiter.start();
    notifier.start();

  5. TIMED_WAITING状态 TIMED_WAITING状态与WAITING状态相似,区别在于线程会在指定的时间间隔后自动唤醒,无需其他线程显式地唤醒它。常见的情况包括使用Thread.sleep(long millis)、Object.wait(long timeout)、Thread.join(long millis)或LockSupport类的相关超时方法。例如:

    Thread t = new Thread(() -> {
        try {
            Thread.sleep(2000); // 线程进入TIMED_WAITING状态,2秒后自动唤醒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    t.start();

  6. TERMINATED状态 当线程正常结束执行,或者因为异常导致线程终止时,线程就会转为TERMINATED状态。在Java程序中,可以通过调用Thread.join()方法来等待一个线程完成执行,并观察其最终状态:

    Thread task = new Thread(() -> {
        // 任务代码
    });

    task.start();
    task.join(); // 等待task线程执行完毕
    System.out.println(task.getState()); // 输出 TERMINATED

    综上所述,Java线程的这六种状态体现了线程生命周期的完整过程,理解这些状态转换对于编写高效的并发程序至关重要。

    Java线程状态之间的转换过程

    1. BLOCKED与RUNNABLE状态间的转换 在多线程并发环境下,当一个线程尝试获取已经被其他线程持有的锁时,它将从RUNNABLE状态转为BLOCKED状态。例如,在Java的synchronized关键字同步块中,线程竞争锁资源的情况如下:

      Object lock = new Object();

      Thread threadA = new Thread(() -> {
          synchronized (lock) {
              try {
                  Thread.sleep(5000); // 持有锁并休眠
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      });

      Thread threadB = new Thread(() -> {
          synchronized (lock) { // 尝试获取已被threadA持有的锁,因此变为BLOCKED状态
              // 执行相关操作
          }
      });

      threadA.start();
      threadB.start();

      // 经过一段时间后,打印线程状态
      while (true) {
          if (threadB.getState() != Thread.State.BLOCKED)
              continue;
          System.out.println("Thread B is now BLOCKED");
          break;
      }

      当线程A释放了对锁的控制权后,线程B会重新变为RUNNABLE状态,并有机会获得CPU时间片执行其代码。

    2. WAITING状态与RUNNABLE状态的转换 线程调用Object.wait()方法或Thread.join()方法会进入WAITING状态,等待被其他线程唤醒。如以下例子所示,线程A在等待线程B结束后才继续执行:

      Thread threadB = new Thread(() -> {
          try {
              Thread.sleep(3000); // 线程B执行一段耗时操作
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      });

      Thread threadA = new Thread(() -> {
          try {
              threadB.join(); // 线程A等待线程B结束,此时线程A为WAITING状态
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          System.out.println("Thread A resumed after thread B finished.");
      });

      threadB.start();
      threadA.start();

      当线程B运行完毕后,线程A将由WAITING状态返回到RUNNABLE状态,进而得以执行。

    3. TIMED_WAITING与RUNNABLE状态的转换 通过调用Thread.sleep(long)、Object.wait(long)、Thread.join(long)等方法,线程可以设定一个超时时间后自动醒来,从而进入TIMED_WAITING状态。当超时时间到达或者提前被其他线程唤醒时,线程会回到RUNNABLE状态。

      Thread threadA = new Thread(() -> {
          try {
              Thread.sleep(2000); // 线程A进入TIMED_WAITING状态,等待2秒
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
          System.out.println("Thread A back to RUNNABLE state after timeout.");
      });

      threadA.start();

    4. 线程中断状态及其处理 Java提供了线程中断机制,允许线程在运行过程中响应中断请求。当调用Thread.interrupt()方法时,线程不会立即停止执行,而是设置其内部的中断标志位。线程可以通过检查自身的中断状态来决定如何响应中断请求。

      Thread threadC = new Thread(() -> {
          while (!Thread.currentThread().isInterrupted()) {
              // 执行任务...
          }
          System.out.println("Thread C interrupted and exiting gracefully.");
      });

      threadC.start();
      // 在某个时刻决定中断线程C
      threadC.interrupt();

      调用Thread.interrupted()会清除当前线程的中断状态并返回当前是否处于中断状态;而Thread.isInterrupted()仅检查当前线程的中断状态而不改变它。在实际应用中,开发者需要设计合理的中断策略以确保线程能够正确地处理中断请求并在适当的时候退出执行。

      注意事项


      在实践中,应当遵循以下几点建议:

      1. 线程中断处理:对可中断的任务,务必检查并响应中断请求,例如在循环体内部调用Thread.currentThread().isInterrupted()来判断并优雅地结束线程执行。
      while (!Thread.currentThread().isInterrupted()) {
          // 执行任务...
      }

      1. 资源协调:充分了解并掌握线程间如何通过锁、条件变量等手段进行有效的通信和协调,防止长时间的不必要等待造成系统瓶颈。
      2. 异常处理:在多线程环境中,任何线程抛出未捕获的异常都会导致该线程直接转为TERMINATED状态,因此要在关键代码段添加合适的异常处理逻辑。
      3. 测试验证:通过编写单元测试和集成测试,模拟不同场景下的线程状态转换,确保线程在各种情况下都能按预期流转,有效避免并发问题。

      总结

      总之,理解并熟练运用Java线程的状态转换原理是提升并发编程能力的关键所在,通过对线程状态的精细控制,可以打造出更健壮、高效的并发应用程序。

      通过深入剖析Java线程的六种状态及其转换过程,我们理解到在多线程编程中,合理管理线程状态对于保证程序正确执行、避免死锁和资源浪费至关重要。针对每个状态:

      • NEW:创建线程后应尽快调用start()方法启动线程,避免出现未初始化的线程实例。
      • RUNNABLE:虽然此状态表示线程可运行或正在运行,但要注意线程间的同步问题,使用synchronized关键字和Lock机制时,可能会导致线程进入BLOCKED状态。
      • BLOCKED:对共享资源进行同步访问时,需要确保适时释放锁以允许其他等待线程继续执行,防止因竞争激烈而导致系统性能下降。
      • WAITING/TIMED_WAITING:在设计线程间协作时,应适当利用Object类的wait/notify以及Thread.join等方法,明确设置超时时间,以便线程在合适时机唤醒或者自动返回RUNNABLE状态。
      • TERMINATED:线程完成任务后应及时处理终止逻辑,如清理资源,并考虑是否需要重新启动或替换新的工作线程。

本文使用 markdown.com.cn 排版

posted @ 2024-01-31 10:41  解码猿  阅读(180)  评论(0编辑  收藏  举报