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

34次阅读

共计 4340 个字符,预计需要花费 11 分钟才能阅读完成。

前言

=====

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过 新建(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 异常

正文完
 0