从零开始学Java之线程的状态是怎么回事?

作者 :孙玉昌,昵称【一一哥 】,另外【壹壹哥】也是我哦

千锋教育高级教研员,CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者

前言

在上一篇文章中,壹哥 给大家介绍了线程中的核心API及其用法,尤其是给大家重点总结了sleep()、yield()、join()、wait()等方法的区别,你还能想起来吗?今天壹哥会给大家讲解线程的几种状态,以及这些状态之间的切换问题,这也是我们开发时必须要掌握的重点内容之一。

------------------------------前戏已做完,精彩即开始----------------------------

全文大约【4000】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......

配套开源项目资料

Github: github.com/SunLtd/Lear...

Gitee: gitee.com/sunyiyi/Lea...

一. 线程状态(生命周期)

我们先来了解一下Java线程的状态到底是怎么回事,看看状态的概念、数量以及状态之间的转换关系。

1. 简介

线程的状态是指线程在不同执行阶段所处的状态。在Java中,Thread类内部给我们提供了一个State枚举类,可以分别表示线程的6种不同状态,如下图所示:

NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED * *这几种状态的具体含义如下:

  1. 新建状态(New) :线程对象被创建但还没有被启动。即start()方法未调用,此时线程并没有开始执行,也没有分配任何系统资源。

  2. 可运行状态(Runnable): 表示线程处于可运行的状态。在早期的很多文章中,有不少人会把Runnable细分为就绪状态(Ready)和运行时状态(Running)

    • 就绪状态(Ready) :线程的start()方法已经被启动,正在等待获取CPU的时间片执行权。此时线程已经分配了系统资源,随时可以被执行。

    • 运行状态(Running) :线程已经获取到了CPU的时间片,正在执行任务。此时线程正在运行中,有可能会被别的线程抢占CPU或自己执行完毕后重新进入到就绪状态。

  1. 阻塞状态(Blocked) :线程因为某种原因暂停执行,等待某种条件的满足或者其他线程释放某个锁,线程处于阻塞状态时就无法执行。根据具体情况,阻塞状态又可以包括以下四种具体的子状态:
    • 等待阻塞(Waitting) :线程等待其他线程的通知或者唤醒,调用wait()方法或join()方法进入等待阻塞状态,只能通过notify()或者notifyAll()方法来唤醒。

    • 同步阻塞(Blocked on Synchronization) :线程试图获得一个对象的锁,但是该锁被其他线程占用,此时线程进入同步阻塞状态。

    • 等待超时阻塞(Timed Waiting) :线程等待其他线程的通知或者唤醒,调用带超时参数的wait()方法或者sleep()方法进入等待超时阻塞状态,当等待时间到达后会自动唤醒。

    • I/O阻塞(Blocked on I/O) :线程执行了某个阻塞式I/O操作时进入I/O阻塞状态。

  1. 等待状态(Waitting) :线程调用wait()方法、join()方法或者LockSupport.park()方法进入到等待状态,等待其他线程的通知或者唤醒。
  2. 超时等待状态(Timed Waiting) :线程调用带有超时参数的wait()方法、join()方法或者sleep()方法进入到超时等待状态,等待一定时间后会自动唤醒。该状态也被称为计时等待或限时等待状态。
  3. 终止状态(Terminated) :线程执行完成或者出现了异常终止执行,此时线程已经释放了所有资源。

线程的这6种状态,其实也就是线程的生命周期,尤其是从线程的创建、运行到消亡,更是典型的生命周期过程,如下图所示:

2. 状态切换

线程的不同状态之间,可以通过一系列的方法或其他操作进行转换。接下来壹哥就把一些状态转换可能性列举出来,如下所示:

  • 就绪状态 -> 运行状态:当CPU资源可用时;
  • 运行状态 -> 就绪状态:当线程执行完自己的任务,或者调用了yield()方法;
  • 运行状态 -> 阻塞状态:当线程需要等待某些操作完成时;
  • 阻塞状态 -> 就绪状态:当线程等待的操作完成时,或者获得了它等待的锁。
  • 运行状态、阻塞状态 -> 等待状态:当线程调用了wait()方法;
  • 等待状态 -> 就绪状态:当线程被notify()或notifyAll()方法唤醒时;
  • 运行状态、阻塞状态、等待状态 -> 终止状态:当线程完成了它的任务或者出现了异常。

我们可以参考下图来理解不同状态之间的切换关系:

二. 状态切换实现案例

1. 基本案例

为了让大家更好地感知线程状态的切换,壹哥通过几个小的案例来给大家演示如何进入这些不同的状态。

1.1 新建状态

在下面的例子中,壹哥创建了一个新的线程,此时线程的状态就是是NEW。

java 复制代码
Thread thread = new Thread();

1.2 可运行状态

在这个例子中,我们先创建了一个新的线程,然后调用start()方法启动线程,此时线程的状态会从NEW变为RUNNABLE状态。

java 复制代码
Thread thread = new Thread();
thread.start();

1.3 阻塞状态

在这个例子中,线程因为没有获取到锁而被阻塞,此时线程的状态会从RUNNABLE变为BLOCKED状态。

java 复制代码
synchronized (lock) {
    // do something
}

1.4 等待状态

在这个例子中,线程因为wait()方法的调用而进入到等待状态,此时线程的状态会从RUNNABLE变为WAITING状态。

java 复制代码
Object obj = new Object();
synchronized (obj) {
    obj.wait();
}

1.5 计时等待状态

在这个例子中,线程因为wait(time)方法的调用而进入到计时等待状态,此时线程的状态会从RUNNABLE变为TIMED_WAITING状态。

java 复制代码
Object obj = new Object();
synchronized (obj) {
    obj.wait(1000);
}

1.6 终止状态

在这个例子中,我们创建了一个新的线程,并在主线程中调用了join()方法等待子线程执行完毕,此时线程的状态会从RUNNABLE变为TERMINATED状态。

java 复制代码
Thread thread = new Thread(() -> {
    // do something
});
thread.start();
thread.join();

2. 综合案例

接下来壹哥再通过一个综合案例,来给大家演示一下线程的几种不同状态。

java 复制代码
/**
 * @author 一一哥Sun
 */
public class Demo12 implements Runnable {
    public synchronized void waitForWhile() throws InterruptedException {
        //等待状态
        System.out.println(Thread.currentThread().getName() + " is waiting state");
		
        //限时等待状态
        wait(2000);
        System.out.println(Thread.currentThread().getName() + " is timed waiting state");
	}

	@Override
	public void run() {
           try {
                Thread.sleep(1000);
                waitForWhile();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
	}

	public static void main(String[] args) throws InterruptedException {
            Demo12 demo = new Demo12();
            // 新建状态
            Thread thread = new Thread(demo);
            System.out.println(thread.getName() + " is in new state");

            // 可运行状态
            thread.start();
            System.out.println(thread.getName() + " is in runnable state");

            // 终止、死亡状态
            Thread.sleep(3000);
            System.out.println(thread.getName() + " is terminated");
        }
}

在上面的示例中,我们创建了一个Demo12类,并实现了Runnable接口。在run()方法中,让当前线程休眠1秒钟,然后调用waitForWhile()方法。在waitForWhile()方法中线程首先进入到等待状态,在等待2秒后会再次执行。然后我们在主线程中首先创建出一个线程对象,打印出线程处于NEW状态;随后我们调用了start()方法,线程处于RUNNABLE状态;然后线程会执行run()方法并调用waitForWhile()方法,线程进入到WAITING状态和TIMED_WAITING状态。最后,我们让主线程休眠3秒钟,此时线程执行完毕,进入到TERMINATED状态。

通过上述案例,我们可以看到,线程状态之间的切换是自动进行的,并且我们可以通过不同的API方法使线程处于不同的状态。

三. 阻塞状态与等待状态

1. 阻塞状态

在以上几种状态中,阻塞状态是一个比较难理解,也是比较重要的状态,壹哥再对其单独解读一下。

在等待某个资源或锁的时候,线程会进入到阻塞状态,在阻塞状态结束后,该线程会进入到就绪状态。而当一个线程被阻塞时,该线程就会暂时停止执行,不再占用CPU时间,因此阻塞状态是一种比较"安静"的状态。一般情况下,线程在等待执行synchronized块时,或等待某个对象的notify()或notifyAll()方法被调用时,就会进入到阻塞状态。根据进入阻塞的不同情况,我们可以把阻塞状态分为两种:

  • 等待阻塞(WAITING) :进入该状态的线程,需要等待其他线程做出一些特定动作(通知或中断),如等待锁、等待输入输出、等待超时等;
  • 同步阻塞(BLOCKED) :进入该状态的线程会等待获取锁。

在Java程序中,我们可以使用synchronized关键字来获取锁,使线程进入到同步阻塞状态,比如下面的代码:

java 复制代码
/**
 * @author 一一哥Sun
 */
public class Demo13 implements Runnable{
    private static final Object lock = new Object();

    @Override
    public void run() {
        //加同步锁
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + "获得锁");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo13 demo = new Demo13();
        Thread thread1 = new Thread(demo);
        Thread thread2 = new Thread(demo);
        thread1.start();
        System.out.println(thread1.getName() + "的状态:" + thread1.getState());
        
        thread2.start();
        Thread.sleep(100);
        System.out.println(thread2.getName() + "的状态:" + thread2.getState());
    }
}

在这个案例的main方法中,我们首先启动一个线程thread1,然后通过Thread.sleep方法暂停100毫秒,接着启动另一个线程thread2。由于在Demo13类的run方法中,使用synchronized关键字获取了对象锁,线程thread1会进入到同步阻塞状态,此时线程thread2尝试获取对象锁就会被阻塞。然后此时如果我们在main方法中,使用thread2.getState()方法获取到线程thread2的状态,此时它的状态是BLOCKED,表示它正在等待获取对象锁。

2. 等待状态

同样的,壹哥也要把等待状态给大家单独解读一下。

当线程处于WAITING状态时,它会等待其他线程执行某些操作,以便继续执行。等待状态通常由以下方法触发:

  • Object.wait()方法:导致当前线程进入等待,直到另一个线程调用了此对象的notify()或notifyAll()方法来唤醒等待线程。
  • Thread.join()方法:使当前线程等待调用该方法的线程执行完毕,然后继续执行。
  • LockSupport.park()方法:禁用当前线程,除非许可证可用,否则将线程置于休眠状态。

当线程处于WAITING状态时,它不会执行任何代码,直到另一个线程通过notify()、notifyAll()或interrupt()方法唤醒它。如果没有其他线程唤醒该线程,它将一直处于WAITING状态。下面是一个简单的示例代码,展示了线程从RUNNABLE状态转换为WAITING状态,并在notify()方法被调用后恢复到RUNNABLE状态的过程。代码如下所示:

java 复制代码
/**
 * @author 一一哥Sun
 */
public class Demo14 {
    public static void main(String[] args) throws InterruptedException {
        //定义一个Object对象
        final Object lock = new Object();
		
        //定义一个线程对象
        Thread thread = new Thread(() -> {
            //添加同步锁
            synchronized (lock) {
                try {
                    System.out.println(Thread.currentThread().getName() + " is waiting ");
                    //进入到WAITING状态
                    lock.wait();			
                    //主线程notify之后,线程thread0被唤醒
                    System.out.println(Thread.currentThread().getName() + " is awake ");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
         });

        //启动子线程thread0
        thread.start();
		
        //一秒钟后,主线程唤醒子线程thread0
        Thread.sleep(1000);
        
        synchronized (lock) {
           // 唤醒线程
           lock.notify(); 
        }
    }
}

在上面的案例中,我们启动一个线程后,主线程等待一秒钟后调用notify()方法,唤醒线程并将其恢复为RUNNABLE状态,线程输出信息后会继续执行。

3. 阻塞和等待状态的区别

壹哥 在上面给大家介绍线程状态的时候,提到线程有阻塞状态和等待状态,这两种状态的特点有些相似,可能有些同学会产生迷惑,所以壹哥 要单独总结一下这两种状态的关系和区别。阻塞状态和等待状态都是线程的一种状态,它们的主要区别在于线程被阻塞或等待的原因不同

  • 当线程请求获取一个锁,但该锁已经被其他线程获取,此时线程就会进入到阻塞状态,等待其他线程释放锁。所以阻塞状态是被动的,也被称为同步阻塞,因为它是由同步机制引起的
  • 而线程进入到等待状态是因为它在等待某个条件的满足,这个条件通常是由其他线程通知唤醒的。比如线程通过调用Object.wait()方法进入到等待状态,等待其他线程调用相同对象的Object.notify()或 Object.notifyAll()方法来唤醒它。这种等待状态是主动的,也称为条件阻塞

总的来说,阻塞状态是由同步机制引起的,而等待状态是由线程自身调用了wait()、join()、park()等方法引起的 。另外,阻塞状态通常是短暂的 ,因为它只是在等待获取某个锁;而等待状态则可能是较长时间的,因为线程等待的条件可能需要较长时间才能满足。

------------------------------正片已结束,来根事后烟----------------------------

四. 结语

今天的这篇文章,壹哥重点给大家介绍了线程的几种状态,以及状态之间的切换方式,尤其是阻塞状态和等待状态。大家掌握这些线程的状态和状态之间的转换,对于理解多线程编程是很重要的。

如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。

相关推荐
daopuyun5 分钟前
GB/T34944-2017 《Java语言源代码漏洞测试规范》解读——安全功能
java·开发语言·安全
编程洪同学9 分钟前
Spring Boot 中实现自定义注解记录接口日志功能
android·java·spring boot·后端
小小药19 分钟前
009-spring-bean的实例化流程
java·数据库·spring
罗政1 小时前
PDF书籍《手写调用链监控APM系统-Java版》第12章 结束
java·开发语言·pdf
GraduationDesign1 小时前
基于SpringBoot的蜗牛兼职网的设计与实现
java·spring boot·后端
今天不学习明天变拉吉1 小时前
大批量数据导入接口的优化
java·excel
小手cool1 小时前
取多个集合的交集
java
颜淡慕潇1 小时前
【K8S问题系列 | 20 】K8S如何删除异常对象(Pod、Namespace、PV、PVC)
后端·云原生·容器·kubernetes
全栈老实人_1 小时前
农家乐系统|Java|SSM|VUE| 前后端分离
java·开发语言·tomcat·maven
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS安康旅游网站(JAVA毕业设计)
java·vue.js·spring boot·后端·kafka·开源·旅游