作者 :孙玉昌,昵称【一一哥 】,另外【壹壹哥】也是我哦
千锋教育高级教研员,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 *。 *这几种状态的具体含义如下:
新建状态(New) :线程对象被创建但还没有被启动。即start()方法未调用,此时线程并没有开始执行,也没有分配任何系统资源。
可运行状态(Runnable): 表示线程处于可运行的状态。在早期的很多文章中,有不少人会把Runnable细分为就绪状态(Ready)和运行时状态(Running) 。
就绪状态(Ready) :线程的start()方法已经被启动,正在等待获取CPU的时间片执行权。此时线程已经分配了系统资源,随时可以被执行。
运行状态(Running) :线程已经获取到了CPU的时间片,正在执行任务。此时线程正在运行中,有可能会被别的线程抢占CPU或自己执行完毕后重新进入到就绪状态。
- 阻塞状态(Blocked) :线程因为某种原因暂停执行,等待某种条件的满足或者其他线程释放某个锁,线程处于阻塞状态时就无法执行。根据具体情况,阻塞状态又可以包括以下四种具体的子状态:
等待阻塞(Waitting) :线程等待其他线程的通知或者唤醒,调用wait()方法或join()方法进入等待阻塞状态,只能通过notify()或者notifyAll()方法来唤醒。
同步阻塞(Blocked on Synchronization) :线程试图获得一个对象的锁,但是该锁被其他线程占用,此时线程进入同步阻塞状态。
等待超时阻塞(Timed Waiting) :线程等待其他线程的通知或者唤醒,调用带超时参数的wait()方法或者sleep()方法进入等待超时阻塞状态,当等待时间到达后会自动唤醒。
I/O阻塞(Blocked on I/O) :线程执行了某个阻塞式I/O操作时进入I/O阻塞状态。
- 等待状态(Waitting) :线程调用wait()方法、join()方法或者LockSupport.park()方法进入到等待状态,等待其他线程的通知或者唤醒。
- 超时等待状态(Timed Waiting) :线程调用带有超时参数的wait()方法、join()方法或者sleep()方法进入到超时等待状态,等待一定时间后会自动唤醒。该状态也被称为计时等待或限时等待状态。
- 终止状态(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()等方法引起的 。另外,阻塞状态通常是短暂的 ,因为它只是在等待获取某个锁;而等待状态则可能是较长时间的,因为线程等待的条件可能需要较长时间才能满足。
------------------------------正片已结束,来根事后烟----------------------------
四. 结语
今天的这篇文章,壹哥重点给大家介绍了线程的几种状态,以及状态之间的切换方式,尤其是阻塞状态和等待状态。大家掌握这些线程的状态和状态之间的转换,对于理解多线程编程是很重要的。
如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。