当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占”着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

新建和就绪状态

当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的 Java 对象一样,仅仅由 Java 虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

当线程对象调用了 start() 方法之后,该线程处于就绪状态, Java 虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于 JVM 里线程调度器的调度。

注意:启动线程使用 start() 方法,而不是 run() 方法!永远不要调用线程对象的 run() 方法!调用 start() 方法来启动线程,系统会 把该 run() 方法当成线程执行体来处理;但如果直接调用线程对象的 run() 方法,则 run() 方法立即就会被执行,而且在 run() 方法返回之前其他线程无法并发执行——也就是说,如果直接调用线程对象的 run() 方法,系统把线程对象当成一个普通对象,而 run() 方法也是一个普通方法,而不是线程执行体。

public class InvokeRun extends Thread {
private int i = 0; // 重写run()方法,方法体就是线程执行体
public void run() {
for (; i < 100; i++) {
// 直接调用run()方法时,Thread的this.getName()返回的是该对象的名字
// 而不是当前线程的名字
// 使用Thread.currentThread().getName()总是获取当前线程的名字
System.out.println(Thread.currentThread().getName() + " " + i);
}
} public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
// 调用Thread的currentThread()方法获取当前线程
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20) {
//直接调用线程对象的run()方法
//系统会把线程对象当成普通对象,把run()方法当成普通方法
//所以下面两行代码并不会启动两个线程,而是依次执行两个run()方法
new InvokeRun().run();
new
InvokeRun().run();
}
}
} }

上面程序创建线程对象后直接调用了线程对象的 run() 方法(如粗体字代码所示),程序运行的结果是整个程序只有一个线程:主线程。还有一点需要指出,如果直接调用线程对象的 run() 方法,则 run() 方法里不能直接通过 getName() 方法来获得当前执行线程的名字,而是需要使用 Thread.currentThread() 方法先获得当前线程,再调用线程对象的 getName() 方法来获得线程的名字。

通过上面程序不难看出,启动线程的正确方法是调用 Thread 对象的 start() 方法,而不是直接调用 run()方法,否则就变成单线程程序了。

需要指出的是,调用了线程的 run() 方法之后,该线程已经不再处于新建状态,不要再次调用线程对象的 start() 方法。

注意:只能对处于新建状态的线程调用 start() 方法,否则将引发 IllegalThreadStateException 异常

调用线程对象的 start() 方法之后,该线程立即进入就绪状态——就绪状态相当于“等待执行”,但该线程并未真正进入运行状态

提示:如果希望调用子线程的 start() 方法后子线程立即开始执行,程序可以使用 Thread.sleep(1) 来让当前运行的线程(主线程)睡眠1毫秒——1毫秒就够了,因为在这1毫秒内 CPU 不会空闲,它会去执行另一个处于就绪状态的线程,这样就可以让子线程立即开始执行。

运行和阻塞状态

如果处于就绪状态的线程获得了 CPU,开始执行 run() 方法的线程执行体,则该线程处于运行状态,

如果计算机只有一个 CPU,那么在任何时刻只有一个线程处于运行状态。当然,在一个多处理器的机器上,将会有多个线程并行(注意是并行: parallel)执行;当线程数大于处理器数时,依然会存在多个线程在同一个 CPU 上轮换的现象。

当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所釆用的策略。对于釆用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。

所有现代的桌面和服务器操作系统都釆用抢占式调度策略,但一些小型设备如手机则可能采用协作式调度策略,在这样的系统中,只有当一个线程调用了它的 sleep() 或 yield() 方法后才会放弃所占用的资源——也就是必须由该线程主动放弃所占用的资源。

当发生如下情况时,线程将会进入阻塞状态。

  • 线程调用 sleep() 方法主动放弃所占用的处理器资源。
  • 线程调用了一个阻塞式 IO 方法,在该方法返回之前,该线程被阻塞。
  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
  • 线程在等待某个通知(notify)。
  • 程序调用了线程的 suspend() 方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。

当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是说,被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它。

针对上面几种情况,当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态。

  • 调用 sleep() 方法的线程经过了指定时间。
  • 线程调用的阻塞式 IO 方法已经返回。
  • 线程成功地获得了试图取得的同步监视器。
  • 线程正在等待某个通知时,其他线程发出了一个通知。
  • 处于挂起状态的线程被调用了 resume() 恢复方法。

从上图可以看出,线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。而就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定,当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入就绪状态。但有一个方法例外,调用 yield() 方法可以让运行状态的线程转入就绪状态。

线程死亡

线程会以如下三种方式结束,结束后就处于死亡状态。

  • run() 或 call() 方法执行完成,线程正常结束。
  • 线程抛出一个未捕获的 Exception 或 Error 。
  • 直接调用该线程的 stop() 方法来结束该线程——该方法容易导致死锁,通常不推荐使用。

注意:当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受主线程的影响。

为了测试某个线程是否已经死亡,可以调用线程对象的 isAlive() 方法,当线程处于就绪、运行、阻塞三种状态时,该方法将返回 true;当线程处于新建、死亡两种状态时,该方法将返回 false。

注意:不要试图对一个已经死亡的线程调用 start() 方法使它重新启动,死亡就是死亡,该线程将不可再次作为线程执行。

public class StartDead extends Thread{
private int i = 0; // 重写run()方法,方法体就是线程执行体
public void run() {
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
} public static void main(String[] args) {
StartDead sd = new StartDead();
for (int i = 0; i < 300; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20) {
//启动线程
sd.start();
//判断启动后的线程的isAlive()值,输出 true
System.out.println(sd.isAlive());
}
//当线程处于新建、死亡两种状态时,isAlive()方法返回 false
//当i>20时,该线程肯定已经启动过了,如果sd.isAlive()为假时
//那就是死亡状态了
if(i>20 && !sd.isAlive()) {
//试图再次启动该线程
sd.start();
}

}
}
}

注意:不要对处于死亡状态的线程调用 start() 方法,程序只能对新建状态的线程调用 start() 方法,对新建状态的线程两次调用 start() 方法也是错误的。这都会引发 IllegalThreadStateException 异常。

Java 线程的生命周期的更多相关文章

  1. Java线程的生命周期(转)

    Java线程的生命周期 一个线程的产生是从我们调用了start方法开始进入Runnable状态,即可以被调度运行状态,并没有真正开始运行,调度器可以将CPU分配给它,使线程进入Running状态,真正 ...

  2. 【Java并发基础】Java线程的生命周期

    前言 线程是操作系统中的一个概念,支持多线程的语言都是对OS中的线程进行了封装.要学好线程,就要搞清除它的生命周期,也就是生命周期各个节点的状态转换机制.不同的开发语言对操作系统中的线程进行了不同的封 ...

  3. Java线程的生命周期

    线程的生命周期包括:新建(New).就绪(Runnable).运行(Running).阻塞(Blocked)和死亡(Dead)5种状态.线程状态转换图如下: 1.新建状态(New) 当程序使用new关 ...

  4. 图解Java线程的生命周期,看完再也不怕面试官问了

    文章首发自个人微信公众号: 小哈学Java https://www.exception.site/java-concurrency/java-concurrency-thread-life-cycle ...

  5. java线程的生命周期及五种基本状态

    一.线程的生命周期及五种基本状态 关于Java中线程的生命周期,首先看一下下面这张较为经典的图: 上图中基本上囊括了Java中多线程各重要知识点.掌握了上图中的各知识点,Java中的多线程也就基本上掌 ...

  6. Java—线程的生命周期及线程控制方法详解

    线程生命周期5种状态 介绍   线程的生命周期经过新建(New).就绪(Runnable).运行(Running).阻塞(Bolocked)和死亡(Dead) 状态转换图 新建(New)   程序使用 ...

  7. Java线程之生命周期

    简述 以下类图展示了线程生命周期中不同的状态.我们可以创建一个线程并启动它,但是线程状态从Runnable.Running.Blocked等状态的变化取决于系统线程调度器,java本身并不能完全控制. ...

  8. Java线程的生命周期与状态流转

    上图是一个线程的生命周期状态流转图,很清楚的描绘了一个线程从创建到终止的过程. 这些状态的枚举值都定义在java.lang.Thread.State下 NEW:毫无疑问表示的是刚创建的线程,还没有开始 ...

  9. 深入Java线程管理(二):线程的生命周期

    Java线程的生命周期 一个线程的产生是从我们调用了start方法开始进入Runnable状态,即可以被调度运行状态,并没有真正开始运行,调度器可以将CPU分配给它,使线程进入Running状态,真正 ...

随机推荐

  1. CentOS 7 上安装vim(默认未安装)

    今天使用CentOS 7,发现未安装vim,所以重新安装 执行命令: yum -y install vim* 然后就可以使用了

  2. Linux之定时任务crond

    定时任务说明与分类 定时任务的应用场景举例 每天晚上 12点备份/etc/目录 tar 定时任务的三种分类 crond(crontab)定时任务软件(软件包cronie),用的最多的一种 atd,应用 ...

  3. PHP设计模式_工厂模式

    个人理解工厂类相当于平时用的基类,只需要把类new一次然后付给一个变量,以后直接引入基类调用变量使用类里的方法即可 了解 工厂模式,工厂方法或者类生成对象,而不是在代码中直接new. 使用工厂模式,可 ...

  4. Flask中无法在其他函数中查询Sqlachemy的解决办法

    报错信息部分截取: File "D:\python 3.5\lib\site-packages\flask_sqlalchemy\__init__.py", line 912, i ...

  5. openvon安装

    一.安装环境 1.阿里云centos7 2.关闭防火墙,selinux 临时关闭:setenforce 0 永久关闭:vim /etc/selinux/config 3.修改yum源 1)备份原有的y ...

  6. 多个窗口开启后,切换到指定title的窗口

    1.在google中,可以开启多个窗口,这是需要切换到自己需要的窗口去定位元素.如下: #获取两个窗口的标题 ${titles} Selenium2Library.Get Window Titles ...

  7. ORA-20011 ORA-29913 KUP-11024 GATHER_TABLE_STATS

    --alter 日志Sat Mar 30 22:01:08 2019DBMS_STATS: GATHER_STATS_JOB encountered errors. Check the trace f ...

  8. 初学Python——装饰器

    一.什么是装饰器 当我们做好一个产品之后,需要对它进行不断地维护,对某些函数增加一些功能.这个时候如果去修改源代码将是非常不合适的.(原因:1.原则上已经写好的函数尽量不去修改它,因为一旦修改可能会导 ...

  9. Java多线程(三)—— synchronized关键字详解

    一.多线程的同步 1.为什么要引入同步机制 在多线程环境中,可能会有两个甚至更多的线程试图同时访问一个有限的资源.必须对这种潜在资源冲突进行预防. 解决方法:在线程使用一个资源时为其加锁即可. 访问资 ...

  10. python推导式创建序列

    推导式创建序列 推导式是一个或多个迭代器快速创建序列的一种方式.可以将循环和条件判断结合,简化代码.几个推导式注意符号的使用,比如小括号,方括号,大括号等等. 列表推导式 列表推导式生成列表对象,语法 ...