啃碎并发:Java线程的生命周期

Posted 猿灯塔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了啃碎并发:Java线程的生命周期相关的知识,希望对你有一定的参考价值。

 

 

 

前言

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

 

技术图片
线程状态转换关系

1 新建(New)状态

当程序使用new关键字创建了一个线程之后,该线程就处于 新建状态,此时的线程情况如下:

 

技术图片
 

2 就绪(Runnable)状态

当线程对象调用了start()方法之后,该线程处于 就绪状态。此时的线程情况如下:

 

技术图片
 

调用start()方法与run()方法,对比如下:

 

技术图片
 
 

如何让子线程调用start()方法之后立即执行而非"等待执行":

 

技术图片
 

3 运行(Running)状态

当CPU开始调度处于 就绪状态 的线程时,此时线程获得了CPU时间片才得以真正开始执行run()方法的线程执行体,则该线程处于 运行状态。

 

技术图片
 

处于运行状态的线程最为复杂,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。线程状态可能会变为阻塞状态、就绪状态和死亡状态。比如:

 

技术图片
 


4 阻塞(Blocked)状态

处于运行状态的线程在某些情况下,让出CPU并暂时停止自己的运行,进入 阻塞状态。

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

 

技术图片
 

阻塞状态分类:



技术图片
 

在阻塞状态的线程只能进入就绪状态,无法直接进入运行状态。而就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定。当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入就绪状态。

 

技术图片
 

4.1 等待(WAITING)状态

线程处于无限制等待状态,等待一个特殊的事件来重新唤醒,如:

 

技术图片
 

以上两种一旦通过相关事件唤醒线程,线程就进入了就绪(RUNNABLE)状态继续运行。

4.2 时限等待(TIMED_WAITING)状态

线程进入了一个时限等待状态,如:

 

技术图片
 

5 死亡(Dead)状态

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

 

技术图片
 

处于死亡状态的线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

所以,需要注意的是:

 

技术图片
 

5.1 终止(TERMINATED)状态

线程执行完毕后,进入终止(TERMINATED)状态。

6 线程相关方法

技术图片
 
技术图片
线程方法状态转换

 

6.1 线程就绪、运行和死亡状态转换

就绪状态转换为运行状态:此线程得到CPU资源;

运行状态转换为就绪状态:此线程主动调用yield()方法或在运行过程中失去CPU资源。

运行状态转换为死亡状态:此线程执行执行完毕或者发生了异常;

注意:

 

技术图片
 

6.2 run & start

通过调用start启动线程,线程执行时会执行run方法中的代码。

 

技术图片
 

6.3 sleep & yield

sleep():通过sleep(millis)使线程进入休眠一段时间,该方法在指定的时间内无法被唤醒,同时也不会释放对象锁;

比如,我们想要使主线程每休眠100毫秒,然后再打印出数字:

 

技术图片
 

注意如下几点问题:

sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。看下面的例子:

 

技术图片
 

Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。

 

技术图片
 

看某一次的运行结果:可以发现,线程0首先执行,然后线程1执行一次,又了执行一次。发现并不是按照sleep的顺序执行的。

 

技术图片
 

yield():与sleep类似,也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出CPU资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。

 

技术图片
 

 

技术图片
 

关于sleep()方法和yield()方的区别如下:

 

技术图片
 

6.4 join

线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。

join有3个重载的方法:

 

技术图片
 

例子代码,如下:

 

技术图片
 

在JDK中join方法的源码,如下:

 

技术图片
 

join方法实现是通过调用wait方法实现。当main线程调用t.join时候,main线程会获得线程对象t的锁(wait 意味着拿到该对象的锁),调用该对象的wait(等待时间),直到该对象唤醒main线程,比如退出后。这就意味着main 线程调用t.join时,必须能够拿到线程t对象的锁。

6.5 suspend & resume (已过时)

suspend-线程进入阻塞状态,但不会释放锁。此方法已不推荐使用,因为同步时不会释放锁,会造成死锁的问题。

resume-使线程重新进入可执行状态。

为什么 Thread.suspend 和 Thread.resume 被废弃了?

Thread.suspend 天生容易引起死锁。如果目标线程挂起时在保护系统关键资源的监视器上持有锁,那么其他线程在目标线程恢复之前都无法访问这个资源。如果要恢复目标线程的线程在调用 resume 之前试图锁定这个监视器,死锁就发生了。这种死锁一般自身表现为“冻结( frozen )”进程。

 

6.6 stop(已过时)

不推荐使用,且以后可能去除,因为它不安全。为什么 Thread.stop 被废弃了?

因为其天生是不安全的。停止一个线程会导致其解锁其上被锁定的所有监视器(监视器以在栈顶产生ThreadDeath异常的方式被解锁)。如果之前被这些监视器保护的任何对象处于不一致状态,其它线程看到的这些对象就会处于不一致状态。这种对象被称为受损的 (damaged)。当线程在受损的对象上进行操作时,会导致任意行为。这种行为可能微妙且难以检测,也可能会比较明显。

不像其他未受检的(unchecked)异常, ThreadDeath 悄无声息的杀死及其他线程。因此,用户得不到程序可能会崩溃的警告。崩溃会在真正破坏发生后的任意时刻显现,甚至在数小时或数天之后。

 

6.7 wait & notify/notifyAll

wait & notify/notifyAll这三个都是Object类的方法。使用 wait ,notify 和 notifyAll前提是先获得调用对象的锁。

 

技术图片
 

前面一直提到两个概念,等待队列(等待池),同步队列(锁池),这两者是不一样的。具体如下:

 

技术图片
 

被notify或notifyAll唤起的线程是有规律的,具体如下:

 

技术图片
 

6.8 线程优先级

每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。

 

技术图片
 

Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~10之间,也可以使用Thread类提供的三个静态常量:

 

技术图片
 

例子代码,如下:

 

技术图片
 

从执行结果可以看到 ,一般情况下,高级线程更显执行完毕。

注意一点:

 

技术图片
 

6.9 守护线程

守护线程与普通线程写法上基本没啥区别,调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。

守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。

setDaemon方法详细说明:

 

技术图片
 

执行结果:

 

技术图片
 

从上面的执行结果可以看出:前台线程是保证执行完毕的,后台线程还没有执行完毕就退出了。

 

技术图片
 

6.10 如何结束一个线程

Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可以使用下面的方法。

 

技术图片
 

比如run方法这样写:只要保证在一定的情况下,run方法能够执行完毕即可。而不是while(true)的无限循环。

 

技术图片
 

诚然,使用上面方法的标识符来结束一个线程,是一个不错的方法,但其也有弊端,如果该线程是处于sleep、wait、join的状态时候,while循环就不会执行,那么我们的标识符就无用武之地了,当然也不能再通过它来结束处于这3种状态的线程了。

所以,此时可以使用interrupt这个巧妙的方式结束掉这个线程。我们先来看看sleep、wait、join方法的声明:

 

技术图片
 

可以看到,这三者有一个共同点,都抛出了一个InterruptedException的异常。在什么时候会产生这样一个异常呢?

 

技术图片
 

看下面的简单的例子:

 

技术图片
 

测试结果:

 

技术图片
 

可以看到,首先执行第一次while循环,在第一次循环中,睡眠2秒,然后将中断状态设置为true。当进入到第二次循环的时候,中断状态就是第一次设置的true,当它再次进入sleep的时候,马上就抛出了InterruptedException异常,然后被我们捕获了。然后中断状态又被重新自动设置为false了(从最后一条输出可以看出来)。

所以,我们可以使用interrupt方法结束一个线程。具体使用如下:

 

技术图片
 

多测试几次,会发现一般有两种执行结果:

 

技术图片
 

或者

 

技术图片
 

这两种结果恰恰说明了,只要一个线程的中断状态一旦为true,只要它进入sleep等状态,或者处于sleep状态,立马回抛出InterruptedException异常。

 

技术图片
 

以上是关于啃碎并发:Java线程的生命周期的主要内容,如果未能解决你的问题,请参考以下文章

Java 并发 线程的生命周期

Java多线程并发02——线程的生命周期与常用方法

Java多线程与并发——线程生命周期和线程池

高并发线程的生命周期其实没有我们想象的那么简单!!

Java并发编程:线程的生命周期是个怎样的过程?

Day829.Java线程的生命周期 -Java 并发编程实战