共计 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 异常。