一、状态简介


一个线程的生命周期里有五大状态,分别是:

  1. 新生
  2. 就绪
  3. 运行
  4. 死亡
  5. 运行后可能遇到的阻塞状态



二、相关方法


2.1 新生状态


Thread t = new Thread();

正如我们前面所说的,一个线程开始之后有自己的内存空间,这些工作空间和主内存进行交互,从主内存拷贝数据到工作空间。

当这个语句执行的时候,线程创建,开辟工作空间,也就是线程进入了新生状态。

2.2 就绪状态


普通情况,一旦调用了:

t.start();

start 方法被调用,线程立即进入了就绪状态,表示这个线程具有了运行的条件,但是还没有开始运行,这就是就绪状态。

线程就绪,但不意味着立即调度执行,因为要等待CPU的调度,一般来说是进入了就绪队列

但是还有另外三种情况,线程也会进入就绪状态,四种分别是:

  1. start()方法调用;
  2. 本来处于阻塞状态,后来阻塞解除;
  3. 如果运行的时候调用 yield() 方法,避免一个线程占用资源过多,中断一下,会让线程重新进入就绪状态。注意,如果调用 yield() 方法之后,没有其他等待执行的线程,此线程就会马上恢复执行;
  4. JVM 本身将本地线程切换到了其他线程,那么这个线程就进入就绪状态。

2.3 运行状态


当CPU选定了一个就绪状态的线程,进行执行,这时候线程就进入了运行状态,线程真正开始执行线程体的具体代码块,基本是 run() 方法。

注意,一定是从 就绪状态 - > 运行状态,不会从阻塞到运行状态的。

2.4 阻塞状态


阻塞状态指的是代码不继续执行,而在等待,阻塞解除后,重新进入就绪状态。

也就是说,阻塞状态发生肯能是运行状态转过去的, 运行状态 - > 阻塞状态,不会从就绪状态转过去。

阻塞的方法有四种:

  1. sleep()方法,是占用资源在睡觉的,可以限制等待多久;
  2. wait() 方法,和 sleep() 的不同之处在于,是不占用资源的,限制等待多久;
  3. join() 方法,加入、合并或者是插队,这个方法阻塞线程到另一个线程完成以后再继续执行;
  4. 有些 IO 阻塞,比如 write() 或者 read() ,因为IO方法是通过操作系统调用的。

上面的方法和start() 一样,不是说调用了就立即阻塞了,而是看CPU。

2.5 死亡状态


死亡状态指的是,线程体的代码执行完毕或者中断执行。一旦进入死亡状态,不能再调用 start() 。

让线程进入死亡状态的方法是 stop() 和 destroy() 但是都不推荐使用,jdk里也写了已过时。

一般的做法是,在线程内,让线程自身自然死亡,或者加一些代码,想办法让线程执行完毕。

  1. 自然死亡:这个线程体里就是多少次的循环,几次调用,执行完了就完了。
  2. 如果不能自然死亡:加一些终止变量,然后用它作为run的条件,这样,外部调用的时候根据时机,把变量设置为false。

比如下面的写法,第一种就是我们的正常写法(虽然很简单但是没有用lambda表达式,主要为了和第二种对比)

/*
终止线程的方法1:自然死亡
*/
public class Status implements Runnable{
@Override
public void run() {
for (int i=0; i<20; i++){
System.out.println("studying");
}
} public static void main(String[] args) {
Status s = new Status();
new Thread(s).start();
}
}
/*
终止线程的方法2:外部控制
*/
public class Status implements Runnable{
//1.加入状态变量
private boolean flag = true;
@Override
//2.关联状态变量
public void run() {
while (flag){
System.out.println("studying");
}
}
//3.对外提供状态变量改变方法
public void terminate() {
this.flag = false;
}
public static void main(String[] args) {
Status s = new Status();//1.新生
new Thread(s).start();//2.就绪,随后进入运行
//无人为阻塞
for (int i=0; i<100000; i++){
if (i==80000){
s.terminate();//3.终止
System.out.println("结束");
}
}
}
}


三、阻塞状态详解


上面的内容,线程的五大状态里,其他四种都比较简单,创建对象的新生态、start开始的就绪态、cpu调度之后进入的运行态,以及正常结束或者外加干预导致的死亡态。

3.1 sleep()


  • sleep(时间)指定当前线程阻塞的毫秒数:
  • sleep存在异常:InterruptException;
  • sleep时间到了之后线程进入就绪状态;
  • sleep可以模拟网络延时、倒计时等;
  • 每一个对象都有一个无形的锁,sleep不会释放锁。(也就是我们说过的,抱着资源睡觉,这个特点对比wait)

前面用线程模拟抢票的网络延时,已经做过示例,就是用sleep设置线程阻塞的时间,达到网络延时的效果,让那个同步问题更容易显现出来。

我们再用龟兔赛跑的例子修改一下,前面兔子和乌龟都是一样各自跑,这次让兔子骄傲的爱睡觉,每隔一段路就睡一段时间。

public class Racer2 implements Runnable{
private String winner;
@Override
public void run() {
for (int dis=1; dis<=100; dis++){
String role = Thread.currentThread().getName();
//模拟兔子睡觉
if (dis%10==0 && role.equals("兔子")){
try {
Thread.sleep(500);//睡
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(role + " 跑了 " + dis);
//每走一步,判断是否比赛结束
if (gameOver(dis))break;
}
} public boolean gameOver(int dis){
if (winner != null){
return true;
} else if (dis == 100){
winner = Thread.currentThread().getName();
System.out.println("获胜者是 "+winner);
return true;
}
return false;
} public static void main(String[] args) {
Racer2 racer = new Racer2();//1.创建实现类
new Thread(racer,"兔子").start();//2.创建代理类并start
new Thread(racer,"乌龟").start();
}
}

这里面:

Thread.sleep(500);//睡

就是让线程进入阻塞状态,并且 500 ms 后,自动进入就绪状态,由 cpu 调度适时重新运行。

也可以利用 sleep 模拟倒计时,不涉及多个线程,所以直接在主线程里面,主方法里写内容就可以,也不用run什么,直接调用sleep设置阻塞时间。

public class SleepTime {
public static void main(String[] args) throws InterruptedException {
int num = 10;
while (num>=0){
Thread.sleep(1000);
System.out.println(num--);
}
}
}

这就会完成10-0 的倒计时。

更花哨一点,用 Dateformat 和 Date 实现时间的显示:

public static void main(String[] args) throws InterruptedException {
//获取十秒后的时间,然后倒计时
Date endTime = new Date(System.currentTimeMillis()+1000*10);
//干预线程的结束
long end = endTime.getTime();
DateFormat format = new SimpleDateFormat("mm:ss");
while (true) {
System.out.println(format.format(endTime));
Thread.sleep(1000);
endTime = new Date(endTime.getTime() - 1000);//-1s
if (end-10000 > endTime.getTime()){
break;
}
}
}

3.2 wait() && notify()


和 sleep() 不同,wait() 方法和 notify() 搭配,可以互相搭配,一个经典的的使用场景以及介绍是在生产者-消费者模型中:

(待更新链接)

3.3 yield()


礼让线程,让当前正在执行的线程暂停,不阻塞线程,而是直接将线程从运行状态转入就绪状态,让cpu调度器重新调度。

用法和 sleep 是一样的,也是一个静态方法,调用起来的效果比 sleep 弱。

这是因为,礼让之余,还有 cpu 的调度在影响顺序,所以无法保证达到线程切换的效果, cpu 还是可能调用当前的线程。

3.4 join()


join 合并线程,待此线程执行完成后,再执行其他线程。也就是说,阻塞其他的所有线程,所以其实应该是插队线程,所以 join 应该翻译成加入,插入?。

不同于 sleep 和 yield 方法,join 不是静态方法,是一个普通方法,要通过一个具体的 Thread 对象才能调用 join。

public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i=0; i<100; i++){
System.out.println("myThread is joining" + i);
}
}); t.start(); for (int i=0; i<100; i++){
if (i == 50){
t.join();//插队,此时主线程不能执行,而要执行 t
}
System.out.println("main is running" + i);
}
}
}

执行结果可以看出来,等到主线程执行到 i = 50后,t 线程完全执行,直到结束,才继续执行 main 线程。

(当然,在 i = 50 之前,是不确定的,cpu 给两个线程的安排)

join 的最重要的部分,就是插队可以保证,这个线程自己一定会先执行完,这是在很多地方需要的逻辑。


四、利用枚举变量监控线程状态


回过头看线程的状态转换图:

新生态、死亡态、除了阻塞内部,其他都已经进行了练习,其中,就绪态到运行态之间是不由程序员控制的,所以 java 给这两个状态了一个统一的名称叫 Runnable(不要和Runnable接口搞混)。

java jdk 里面对于线程状态的区分:

  1. NEW 对应没有 Started 的线程,对应新生态;
  2. RUNNABLE,对于就绪态和运行态的合称;
  3. BLOCKED,WAITING,TIMED_WAITING三个都是阻塞态:
    • sleep 和 join 称为WAITING,TIMED_WAITING(设置了时间的话);
    • wait 和 IO 流阻塞称为BLOCKED。
  4. TERMINATED 死亡态。

我们用一个 demo 观察一下状态的切换过程:

public class AllState {
public static void main(String[] args) throws InterruptedException {
//一个拥有阻塞的线程
Thread t = new Thread(()->{
for (int i=0; i<10; i++){
try {
Thread.sleep(200);//阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("........I am running.........");
}
}); System.out.println(t.getState());//获取状态,此时应该是new t.start();
System.out.println(t.getState());//获取状态,此时已经start //监控阻塞,设置结束条件为监控到 t 线程已经变成 Terminated
while (!t.getState().equals(Thread.State.TERMINATED)){
Thread.sleep(200);//主线程每200ms监控一次线程 t
System.out.println(t.getState());
}
}
}

从输出结果可以看到,从 开始的 new 到 runnable(start后),到有线程执行了 running 的runnable,到阻塞的 timed_waiting ,到恢复 runnable,到最终的结束 terminated。

Thread 还提供了线程数方法,可以计数,结束条件其实还可以改成对于线程数的判断,因为当 t 结束后,线程数就只剩下主线程了

while (Thread.activeCount() != 1){
System.out.println(Thread.activeCount());
Thread.sleep(200);//主线程每200ms监控一次线程 t
System.out.println(t.getState());
}

然而,运行起来的时候输出显示的是 3 个线程:

最后 terminated 之后陷入了线程数是 2 的死循环,和预想的不一样。。。

引入了另一个问题,搜了一下,应该是控制台输出,也是一个线程被监控的。

java线程的五大状态,阻塞状态详解的更多相关文章

  1. java线程池的使用与详解

    java线程池的使用与详解 [转载]本文转载自两篇博文:  1.Java并发编程:线程池的使用:http://www.cnblogs.com/dolphin0520/p/3932921.html   ...

  2. Java线程创建形式 Thread构造详解 多线程中篇(五)

    Thread作为线程的抽象,Thread的实例用于描述线程,对线程的操纵,就是对Thread实例对象的管理与控制. 创建一个线程这个问题,也就转换为如何构造一个正确的Thread对象. 构造方法列表 ...

  3. Java线程池(ThreadPool)详解

    线程五个状态(生命周期): 线程运行时间 假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间.    如果:T1 + T3 远大于 T2,则可以 ...

  4. Java线程池七个参数详解

    Java多线程开发时,常常用到线程池技术,这篇文章是对创建java线程池时的七个参数的详细解释. 从源码中可以看出,线程池的构造函数有7个参数,分别是corePoolSize.maximumPoolS ...

  5. 【多线程】Java线程池七个参数详解

    /** * Creates a new {@code ThreadPoolExecutor} with the given initial * parameters. * * @param coreP ...

  6. java线程池ThreadPoolExecutor类使用详解

    在<阿里巴巴java开发手册>中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量:另一方面线程的细节管理交给线 ...

  7. java线程基础知识----SecurityManager类详解

    在查看java Thread源码的时候发现一个类----securityManager,虽然很早就知道存在这样一个类但是都没有深究,今天查看了它的api和源码,发现这个类功能强大,可以做很多权限控制策 ...

  8. Java线程Thread的状态解析以及状态转换分析 多线程中篇(七)

    线程与操作系统中线程(进程)的概念同根同源,尽管千差万别. 操作系统中有状态以及状态的切换,Java线程中照样也有. State 在Thread类中有内部类 枚举State,用于抽象描述Java线程的 ...

  9. java的集合框架最全详解

    java的集合框架最全详解(图) 前言:数据结构对程序设计有着深远的影响,在面向过程的C语言中,数据库结构用struct来描述,而在面向对象的编程中,数据结构是用类来描述的,并且包含有对该数据结构操作 ...

随机推荐

  1. vue学习(十一) v-for使用的注意事项:2.2.0+之后的版本里,当在组件中使用v-for时,key是必须的,它是用来表示唯一身份的

    //html <div id="app"> <div> <label>id <input type="text" v- ...

  2. Cypress系列(41)- Cypress 的测试报告

    如果想从头学起Cypress,可以看下面的系列文章哦 https://www.cnblogs.com/poloyy/category/1768839.html 注意 51 testting 有一篇文章 ...

  3. 日志分析-利用grep,awk等文本处理工具完成(2019-4-9)

    0x00 基础日志分析命令 1. tail - 监控末尾日志的变化 $tail -n 10 error2019.log #显示最后10行日志内容 $tail -n +5 nginx2019.log # ...

  4. 官宣!AWS Athena正式可查询Apache Hudi数据集

    1. 引入 Apache Hudi是一个开源的增量数据处理框架,提供了行级insert.update.upsert.delete的细粒度处理能力(Upsert表示如果数据集中存在记录就更新:否则插入) ...

  5. IO—》File类

    IO概述 回想之前写过的程序,数据都是在内存中,一旦程序运行结束,这些数据都没有了,等下次再想使用这些数据,可是已经没有了.那怎么办呢?能不能把运算完的数据都保存下来,下次程序启动的时候,再把这些数据 ...

  6. 萌新学渗透系列之Hack The Box_Legacy

    我将我的walkthrough过程用视频解说的形式记载 视频地址https://www.bilibili.com/video/BV1mZ4y1u7jG 一是因为看我视频的后来者应该都是刚入门的新手,视 ...

  7. Servlet学习之Maven导入Servlet-api包与Tomcat冲突报500问题

    Maven导入Servlet-api包导致无法运行报500的问题 以下解决方式适用于跟我类似的配置环境,构建servlet程序后,启动Tomcat页面报"类com.tioxy.servlet ...

  8. Python更新列表

    Python更新列表: 使用索引下标进行更新: # 修改列表的第 6 个元素为 d lst = ['a','b','c',1,2,3] lst[5] = 'd' print(lst) # ['a', ...

  9. PHP lstat() 函数

    定义和用法 lstat() 函数返回关于文件或符号连接的信息. 该函数将返回一个包含下列元素的数组: [0] 或 [dev] - 设备编号 [1] 或 [ino] - inode 编号 [2] 或 [ ...

  10. PHP rewind() 函数

    定义和用法 rewind() 函数将文件指针的位置倒回文件的开头. 如果成功,该函数返回 TRUE.如果失败,则返回 FALSE. 语法 rewind(file) 参数 描述 file 必需.规定已打 ...