线程的生命周期以及其中的方法

线程的生命周期

线程的生命周期分为新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)这 5 种状态。在系统运行过程当中不断会有新的线程被创建,旧的线程在执行完毕后被清理,线程在排队获取共享资源或者锁时将被阻塞,因此运行中线程就是在就绪、阻塞、运行状态之间来回切换。线程的具体状态转换流程如下图所示:
在这里插入图片描述

其执行流程:

  1. 调用 new 方法创建一个线程,这时线程处于新建状态。
  2. 调用 start 方法启动一个线程,这时线程处于就绪状态。
  3. 处于就绪状态的线程等待线程获取到 CPU 资源,在等待其获取 CPU 资源后线程会执行 run() 方法进行运行状态。
  4. 正在执行的线程在调用 yield() 方法或失去处理资源时,会再次进入就绪状态。
  5. 正在执行的线程在执行 sleep() 方法、I/O 阻塞、等待同步锁、等待通知、调用 suspeed() 方法等操作后,会挂起并进入阻塞状态,进入 Blocked 池。
  6. 阻塞状态的线程由于出现 sleep 时间已到、I/O 方法返回、获得同步锁、收到通知、调用 resume 方法等情况,会再次进入就绪状态,等待 CPU 时间片的轮询。该线程在获取 CPU 资源后,会再次进入到运行状态。
  7. 处于运行状态的线程,在调用 run 方法或 call 方法正常执行完成、调用 stop 方法停止下线程或者程序执行错误导致异常退出时,会进入死亡状态。

线程的5种状态

新建状态 New
在 Java 当中使用 new 关键字创建了一个线程,新创建的线程将处于新建状态。在创建线程的时候主要是为线程分配内存并初始化其成员变量的值。

就绪状态 Runnable
新建的线程对象在调用 start 方法之后将转为就绪状态。此时 JVM 完成了方法调用栈和程序计数器的创建,等待该线程的调度和运行。

运行状态 Running
就绪状态的线程在竞争到 CPU 的使用权并开始执行 run() 方法的线程执行体时,会转为运行状态,处于运行状态的线程的主要任务就是执行 run() 方法中的逻辑代码。

阻塞状态 Blocked
运行中的线程会主动或被动地抛弃 CPU 的使用权并暂停运行,此时该线程将转为阻塞状态,直到再次进入可运行状态,才有机会再次竞争到 CPU 使用权并转为运行状态。阻塞的状态可以分为以下三种:

  1. 等待阻塞:在运行态的线程调用 o.wait() 方法时,JVM 会把该线程放入等待队列 (Waitting Queue)中,线程转为阻塞状态。
  2. 同步阻塞:在运作状态的线程尝试获取正在被其他线程占用的对象同步锁时, JVM 会把线程放入到锁池当(Lock Pool)中,此时线程装换为阻塞状态。
  3. 其他阻塞:运行状态的线程在执行 Thread.sleep(long ms)、Thread.join()或者发出 I/O 请求时, JVM 会把该线程转为阻塞状态。直到 sleep() 状态超时、Thread.join()等待线程终止或超时,或者 I/O 处理完毕,线程才重新转为可运行状态。

线程死亡 Dead
线程在以下三种方式结束后转换为死亡状态

线程正常结束:run 方法或 call 方法执行完成
线程异常退出:运行中的线程抛出一个 Error 或未捕获的 Exception,线程异常退出。
手动结束:调用线程对象的 stop 方法手动结束运行中的线程(该方式会瞬间释放线程占用的同步对象锁,导致锁混乱和死锁,不推荐使用)

线程的基本方法

线程相关的基本方法有 wait、notify、notifyAll、sleep、join、yield 等,这些方法控制线程的运行,并影响线程状态之间的转换。

线程等待:wait 方法

调用 wait() 方法的线程会进入 WAITING 状态,只有等到其他线程的通知或被中断后才会返回。需要注意的是,在调用 wait() 方法后会释放对象的锁,因此 wait() 方法一般是被使用与同步方法或同步代码块当中。

线程睡眠:sleep 方法

调用 sleep 方法会导致当前线程休眠。与 wait() 方法不同的是,sleep() 方法不会释放当前占有的锁,会导致线程进入 TIMED-WAITING 状态,而 wait 方法会导致当前线程进入 WAITING 状态。

线程让步:yield 方法

调用 yield 方法会使当前线程让出(释放)CPU 执行的时间片,与其他线程一起重新竞争 CPU 时间片。在一般情况下,优先级高的线程更有可能竞争到 CPU 时间片,但这不是绝对的,有的操作系统对线程的优先级并不敏感。

线程中断:interrupt 方法

interrupt 方法用于向线程发行一个终止通知信号,会影响该线程内部的一个中断标识位,线程本身并不会因为调用了 interrupt 方法而改变状态(阻塞、终止等)。状态的具体变化需要等待接收到中断标识的程序的最终结果来判定。对 interrupt 方法的理解需要注意以下 4 个核心的问题:

  1. 调用 interrupt 方法并不会中断一个正在运行的线程,也就是说处于 Running 状态的线程并不会因为调用了 interrupt 方法而终止,仅仅改变了内部维护的中断标识位而已。
    JDK 源码:
    public static boolean interrupted() {
    
    
        return currentThread().isInterrupted(true);
    }
    public boolean isInterrupted() {
    
    
        return isInterrupted(false);
    }
  1. 若因为调用了 sleep 方法而使线程处于 TIMED-WATING 状态,则这时调用 interrupt 方法会抛出 InterruptedException,使线程提前结束 TIME-WATING 状态。
  2. 许多声明抛出 InterruptedException 的方法如 Thread.sleep(long mills),在抛出异常前都会清除中断标识位,所以在抛出异常后调用 isInterrupted 方法将会返回 false。
  3. 中断状态是线程所固有的一个标识位,可以通过此标识位安全终止线程。比如,在想终止一个线程的时候,可以先调用该线程的 interrupt 方法,然后在线程的 run 方法中根据该线程 isInterrupted 方法的返回状态值安全终止线程。
public class ThreadMethod {
    
    
    public static void main(String[] args) {
    
    
        ThreadDemo threadDemo = new ThreadDemo();
        threadDemo.start();
        threadDemo.interrupt();
    }
}

class ThreadDemo extends Thread {
    
    

    @Override
    public void run() {
    
    
        // 开始时线程中断标识状态为:
        System.out.println("初始默认的标识为:" + Thread.currentThread().isInterrupted());
        if (Thread.currentThread().isInterrupted()) {
    
    
            try {
    
    
                System.out.println("线程sleep睡眠1秒");
                sleep(1000);//在这sleep了。就将线程中断改为 false,抛出异常,并立刻结束睡眠
            } catch (InterruptedException e) {
    
    
                System.out.println("出现异常");
                // 线程中断异常标识为:
                System.out.println("出现异常后标识符为:" + Thread.currentThread().isInterrupted());
                Thread.currentThread().interrupt(); //重新设置中断标识
            }
        }

        System.out.println("重置后标识符为:" + Thread.currentThread().isInterrupted());
        if (Thread.currentThread().isInterrupted()) {
    
    
            try {
    
    
                sleep(1000);//抛出异常
            } catch (InterruptedException e) {
    
    
                System.out.println("因为sleep抛出异常");
                e.printStackTrace();
            }
        }
        System.out.println("---------当前线程任务执行完啦---------");
    }
}

在这里插入图片描述

线程加入:join 方法

join 方法是用于等待其他线程终止,如果在当前线程中调用一个线程的 join 方法,则当前线程会转为阻塞状态,等到另一个线程结束,当前线程再由阻塞状态转为就绪状态,等待获取 CPU 的使用权。
在很多情况下,主线程生成并启动子线程,需要等待子线程返回结果并收集和处理再退出,这时就要用到 join 方法。

System.out.println("这个是主线程");
// 生成子线程
ChildThread childThread = new ChildThread();
// 执行子线程
childThread.join();//主线程(当前线程)阻塞等待子线程 childThread 执行线束
System.out.println("子线程join启动后,执行完毕后开始运行主线程");

线程唤醒:notify 方法

Object 类有个 notify 方法,用于唤醒在此时对象监视器上等待的一个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,唤醒时选择是任意的。

我们通常调用其中一个对象的 wait 方法在对象的监视器上等待,直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方法与在该对象上主动同步的其他线程竞争。类似的方法还有 notifyAll ,用于唤醒在监视器上等待的所有线程。

后台守护线程:setDaemon 方法

setDaemon 方法用于定义一个守护线程,也叫做“服务线程”,该线程是后台线程。后台守护线程有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开。

守护类线程的优先级较低,用于为系统中的其他对象和线程提供服务。将一个用户线程设置为守护现场的方法是在线程对象创建之前用线程对象的 setDacemon(true) 来设置的。

在后台守护线程中定义线程也是后台守护线程。后台守护线程是 JVM 级别的,比如垃圾回收线程就是一典型的守护线程,在我们的程序中不再有任何线程运行时,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以在回收 JVM 上仅剩的线程时,垃圾回收器线程会自动离开。它始终在低级别的状态下运行,用于实时监控和管理系统中的可回收资源。

守护线程是运行在后台的一种特殊线程,其独立于控制终端并且周期性地执行某种任务或等待处理某些已发生的事件,也就是说,守护线程是不依赖于终端的,但是依赖于 JVM ,与 JVM “同生共死”。在 JVM 中的所有线程都是守护线程时,JVM 就可以退出了,如果还有一个或一个以上的非守护现场,则 JVM 是不会退出的。


这些方法对线程的影响:
在这里插入图片描述

sleep 和wait 方法的区别

  1. sleep 方法是属于 Thread 类,wait 方法是属于 Object 类。
  2. sleep 方法暂停执行指定时间,让出 CPU 给其他线程,但其监控状态依然保持,在指定的时间过后自动回复为运作状态。
  3. 在调用 sleep 方法的过程中,线程不会释放对象锁。
  4. 在调用 wait 方法时,线程会放弃对象锁,进入等待此对象的等待锁池,只有针对此对象调用 notify 方法后,该线程才能进入对象锁池准备获取对象锁,并进入运行状态。

start 和 run 方法的区别

  1. start 方法用于启动线程,真正实现了多线程运行。在调用了线程的 start 方法后,线程会在后台执行,无须等待 run 方法体的代码执行玩不,就可以继续执行下面的代码。
  2. 在通过调用 Thread 类的 start 方法启动一个线程时,此线程时处于就绪状态,并没有运行。
  3. run 方法也叫做线程体,包含了要执行的线程的逻辑代码,在调用 run 方法后,线程会进入运行状态,开始运行 run 方法中的代码。在run 方法运行结束后,该线程终止, CPU 再次调度其他线程。

下一章:===》线程终止的 4 种方式

猜你喜欢

转载自blog.csdn.net/weixin_45970271/article/details/125488902
今日推荐