浅谈 Java 线程状态转换及控制
一个线程被创建后就进入了线程的生命周期。在线程的生命周期中,共包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)这五种状态。当线程启动以后,CPU 需要在多个线程之间切换,所以线程也会随之在运行、阻塞、就绪这几种状态之间切换。
线程的状态转换如图:
当使用 new 关键字创建一个线程对象后,该线程就处于新建状态。此时的线程就是一个在堆中分配了内存的静态的对象,线程的执行体(run 方法的代码)不会被执行。
当调用了线程对象的 start()方法后,该线程就处于就绪状态。此时该线程并没有开始运行,而是处于可运行池中,Java 虚拟机会为该线程创建方法调用栈和程序计数器。至于该线程何时才能运行,要取决于 JVM 的调度。
一旦处于就绪状态的线程获得 CPU 开始运行,该线程就进入了运行状态。线程运行时会执行 run 方法的代码。对于抢占式策略的操作系统,系统会为每个可执行的线程分配一个时间片,当该时间片用尽后,系统会剥夺该线程所占有的处理器资源,从而让其他线程获得占有 CPU 而运行的机会。此时该线程会从运行态转为就绪态。
当一个正在运行的线程遇到如下情况时,线程会从运行态转为阻塞态:
① 线程调用 sleep、join 等方法。
② 线程调用了一个阻塞式 IO 方法。
③ 线程试图获得一个同步监视器,但是该监视器正在被其他线程持有。
④ 线程在等待某个 notify 通知。
⑤ 程序调用了线程的 suspend 方法将该线程挂起。
当线程被阻塞后,其他线程就有机会获得 CPU 资源而被执行。当上述导致线程被阻塞的因素解除后,线程会回到就绪状态等待处理机调度而被执行。
当一个线程执行结束后,该线程进入死亡状态。
有以下 3 种方式可结束一个线程:
① run 方法执行完毕。
② 线程抛出一个异常或错误,而该异常或错误未被捕获。
③ 调用线程的 stop 方法结束该线程。(不推荐使用)
2|0 线程的控制
Thread 类中提供了一些控制线程的方法,通过这些方法可以轻松地控制一个线程的执行和运行状态,以达到程序的预期效果。
2|1join 方法
如果线程 A 调用了线程 B 的 join 方法,线程 A 将被阻塞,等待线程 B 执行完毕后线程 A 才会被执行。这里需要注意一点的是,join 方法必须在线程 B 的 start 方法调用之后调用才有意义。join 方法的主要作用就是实现线程间的同步,它可以使线程之间的并行执行变为串行执行。
join 方法有以下 3 种重载形式:
① join(): 等待被 join 的线程执行完成。
② join(long millis): 等待被 join 的线程的时间为 millis 毫秒,如果该线程在 millis 毫秒内未结束,则不再等待。
③ join(long millis,int nanos): 等待被 join 的线程的时间最长为 millis 毫秒加上 nanos 微妙。
我们定义了一个 JoinThread 类,它继承了 Thread 类,这是我们要加入的线程类。
在 main 方法中,我们创建了 JoinThread 线程,并把它加入到当前线程(主线程)中,并没有指定当前线程等待的时间,所以会一直阻塞当前线程,直到 JoinThread 线程的 run 方法执行完毕,才会继续执行当前线程。
2|2sleep 方法
当线程 A 调用了 sleep 方法,则线程 A 将被阻塞,直到指定睡眠的时间到达后,线程 A 才会重新被唤起,进入就绪状态。
sleep 方法有以下 2 种重载形式:
① sleep(long millis):让当前正在执行的线程暂停 millis 毫秒,该线程进入阻塞状态。
② sleep(long mills,long nanos):让当前正在执行的线程暂停 millis 毫秒加上 nanos 微秒。
这段代码中没有创建其他线程,只有当前线程存在,也就是执行 main 函数的主线程。for 循环中每打印一次线程名称,主线程就会被 sleep 方法阻塞 1s,然后进入就绪状态,重新等待被调到,实现了线程的控制。
2|3yield 方法
当线程 A 调用了 yield 方法,它可以暂时放弃处理器,但是线程 A 不会被阻塞,而是进入就绪状态。
我们自定义了一个线程类 YieldThread,在 run 方法中定义了一个 for 循环,for 循环中每打印一次线程名称,就会调用一下 yield 方法,主动放弃 CUP 让给其它有相同优先级或更高优先级的线程,自己进入就绪状态,等待被 CPU 调度。
2|4 设置线程的优先级
每个线程都有自己的优先级,默认情况下线程的优先级都与创建该线程的父线程的优先级相同。同时 Thread 类提供了 setPriority(int priority) 和 getPriority()方法设置和返回指定线程的优先级。参数 priority 是一个整型数据,用以指定线程的优先级。priority 的取值范围是 1-10,默认值为 5,也可以使用 Thread 类提供的三个静态常量设置线程的优先级。
① MAX_PRIORITY:最高优先级,其值为 10。
② MIN_PRIORITY:最低优先级,其值为 1。
③ NORM_PRIORITY:普通优先级,其值为 5。
我们创建了三个线程 p1、p2、p3,设置了 p1 的优先级为 1,p3 的优先级为 10,并没有设置 p2 的,所以 p2 的优先级默认是 5。优先级越高,表示获取 cup 的机会越多,注意此处说的是机会,所以高优先级的线程并不是一定先于低优先级的线程被 CPU 调度,只是机会更大而已。
sleep 方法和 wait 方法的区别是什么?
sleep 方法是 Thread 类的一个静态方法,其作用是使运行中的线程暂时停止指定的毫秒数,从而该线程进入阻塞状态并让出处理器,将执行的机会让给其他线程。但是这个过程中监控状态始终保持,当 sleep 的时间到了之后线程会自动恢复。
wait 方法是 Object 类的方法,它是用来实现线程同步的。当调用某个对象的 wait 方法后,当前线程会被阻塞并释放同步锁,直到其他线程调用了该对象的 notify 方法或者 notifyAll 方法来唤醒该线程。所以 wait 方法和 notify(或 notifyAll)应当成对出现以保证线程间的协调运行。
sleep 方法和 yield 方法的区别是什么?
① sleep 方法暂停当前线程后,会给其他线程执行机会而不会考虑其他线程的优先级。但是 yield 方法只会给优先级相同或者优先级更高的线程执行机会。
② sleep 方法执行后线程会进入阻塞状态,而执行了 yield 方法后,当前线程会进入就绪状态。
③ 由于 sleep 方法的声明抛出了 InterruptedException 异常,所以在调用 sleep 方法时需要 catch 该异常或抛出该异常,而 yield 方法没有声明抛出异常。
④ sleep 方法比 yield 方法具有更好的可移植性。
补充一下 sleep、yield、join 和 wait 的差异:
① sleep、join、yield 时并不释放对象锁资源,在 wait 操作时会释放对象资源,wait 在被 notify/notifyAll 唤醒时,重新去抢夺获取对象锁资源。
② sleep、join、yield 可以在任何地方使用,而 wait,notify,notifyAll 只能在同步控制方法或者同步控制块中使用。
③ 调用 wait 会立即释放锁,进入等待队列,但是 notify()不会立刻释放 sycronized(obj)中的对象锁,必须要等 notify()所在线程执行完 sycronized(obj)同步块中的所有代码才会释放这把锁,然后供等待的线程来抢夺对象锁。
Java 中为什么不建议使用 stop 和 suspend 方法终止线程?
在 Java 中可以使用 stop 方法停止一个线程,使该线程进入死亡状态。但是使用这种方法结束一个线程是不安全的,在编写程序时应当禁止使用这种方法。
之所以说 stop 方法是线程不安全的,是因为一旦调用了 Thread.stop()方法,工作线程将抛出一个 ThreadDeath 的异常,这会导致 run 方法结束执行,而且结束的点是不可控的,也就是说,它可能执行到 run 方法的任何一个位置就突然终止了。同时它还会释放掉该线程所持有的锁,这样其他因为请求该锁对象而被阻塞的线程就会获得锁对象而继续执行下去。一般情况下,加锁的目的是保护数据的一致性,然而如果在调用 Thread.stop()后线程立即终止,那么被保护数据就有可能出现不一致的情况(数据的状态不可预知)。同时,该线程所持有的锁突然被释放,其他线程获得同步锁后可以进入临界区使用这些被破坏的数据,这将有可能导致一些很奇怪的应用程序错误发生,而且这种错误非常难以 debug.所以在这里再次重申,不要试图用 stop 方法结束一个线程。
suspend 方法可以阻塞一个线程,然而该线程虽然被阻塞,但它仍然持有之前获得的锁,这样其他任何线程都不能访问相同锁对象保护的资源,除非被阻塞的线程被重新恢复。如果此时只有一个线程能够恢复这个被 suspend 的线程,但前提是先要访问被该线程锁定的临界资源。这样便产生了死锁。所以在编写程序时,应尽量避免使用 suspend,如确实需要阻塞一个线程的运行,最好使用 wait 方法,这样既可以阻塞掉当前正在执行的线程,同时又使得该线程不至于陷入死锁。
用一句话说就是:stop 方法是线程不安全的,可能产生不可预料的结果;suspend 方法可能导致死锁。
如何终止一个线程?
在 Java 中不推荐使用 stop 方法和 suspend 方法终止一个线程,因为那是不安全的,那么要怎样终止一个线程呢?
方法一:使用退出标志
正常情况下,当 Thread 或 Runnable 类的 run 方法执行完毕后该线程即可结束,但是有些情况下 run 方法可能永远都不会停止,例如,在服务端程序中使用线程监听客户端请求,或者执行其他需要循环处理的任务。这时如果希望有机会终止该线程,可将执行的任务放在一个循环中(例如 while 循环),并设置一个 boolean 型的循环结束的标志。如果想使 while 循环在某一特定条件下退出,就可以通过设置这个标志为 true 或 false 来控制 while 循环是否退出。这样将线程结束的控制逻辑与线程本身逻辑结合在一起,可以保证线程安全可控地结束。
让我们来看一看案例:
在上面这段程序中的 main 方法里创建了两个线程,第一个线程的 run 方法中有一个 while 循环,该循环通过 boolean 型变量 quitFlag 控制其是否结束。因为变量 quitFlag 的初始值为 false,所以如果不修改该变量,第一个线程中的 run 方法将不会停止,也就是说,第一个线程将永远不会终止,并且每隔 1s 在屏幕上打印出一条字符串。第二个线程的作用是通过修改变量 quitFlag 来终止第一个线程。在第二个线程的 run 方法中首先将线程阻塞 3s,然后将 quitFlag 置为 true.因为变量 quitFlag 是同一进程中两个线程共享的变量,所以可以通过修改 quitFlag 的值来控制第一个线程的执行。当变量 quitFlag 被置为 true,第一个线程的 while 循环就可以终止,所以 run 方法就能执行完毕,从而安全退出第一个线程。
注意,boolean 型变量 quitFlag 被声明为 volatile,volatile 会保证变量在一个线程中的每一步操作在另一个线程中都是可见的,所以这样可以确保将 quitFlag 置为 true 后可以安全退出第一个线程。
方法二:使用 interrupt 方法
使用退出线程标志的方法终止一个线程存在一定的局限性,主要的限制就是这种方法只对运行中的线程起作用,如果该线程被阻塞(例如,调用了 Thread.join()方法或者 Thread.sleep()方法等)而处于不可运行的状态时,则退出线程标志的方法将不会起作用。
在这种情况下,可以使用 Thread 提供的 interrupt()方法终止一个线程。因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,然后通过 catch 块捕获该异常,从而安全地结束该线程。
我们来看看下面的例子:
在上面这段程序中的 main 方法里创建了一个线程,在该线程的 run 方法中调用 sleep 函数将该线程阻塞 10s.然后调用 Thread 类的 start 方法启动该线程,该线程刚刚被启动就进入阻塞状态。主线程等待 1s 后调用 thread.interrupt()抛出一个中断信号,在 run 方法中的 catch 会正常捕获到这个中断信号,这样被阻塞的该线程就会提前退出阻塞状态,不需要等待 10s 线程 thread 就会被提前终止。
上述方法主要针对当前线程调用了 Thread.join()或者 Thread.sleep()等方法而被阻塞时终止该线程。如果一个线程被 I/O 阻塞,则无法通过 thread.interrupt()抛出一个中断信号而离开阻塞状态。这时可推而广之,触发一个与当前 I/O0 阻塞相关的异常,使其退出 I/O 阻塞,然后通过 catch 块捕获该异常,从而安全地结束该线程。
总结一下:
当一个线程处于运行状态时,可通过设置退出标志的方法安全结束该线程;当一个线程被阻塞而无法正常运行时,可以抛出一个异常使其退出阻塞状态,并 catch 住该异常从而安全结束该线程。
3|0 线程的状态(JVM 层面)
我们在上面讨论的线程状态是从操作系统层面来看的,这样看比较直观,也容易理解,也是一个线程在操作系统中真实状态的体现。下面我们来看看 Java 中线程的状态及转换。
3|1Java 线程状态
在 Java 中线程的状态有 6 种,我们来看一看 JDK 1.8 帮助文档中的说明:
JDK1.8 帮助文档-线程状态
我们可以看到帮助文档中的最后一行,这些状态是不反映任何操作系统线程状态的 JVM 层面的状态。我们来具体看一看这六种状态:
NEW:初始状态,线程被创建,但是还没有调用 start 方法。
RUNNABLED:运行状态,JAVA 线程把操作系统中的就绪和运行两种状态统称为“运行状态”。
BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了 CPU 使用权,阻塞也分为几种情况 :
等待阻塞:运行的线程执行了 Thread.sleep 、wait()、 join() 等方法 JVM 会把当前线程设置为等待状态,当 sleep 结束、join 线程终止或者 wait 线程被唤醒后,该线程从等待状态进入到阻塞状态,重新抢占锁后进行线程恢复;
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么 jvm 会把当前的线程放入到锁池中 ;
其他阻塞:发出了 I/O 请求时,JVM 会把当前线程设置为阻塞状态,当 I/O 处理完毕则线程恢复;
WAITING:等待状态,没有超时时间,要被其他线程唤醒或者有其它的中断操作;
执行 wait()
执行 join()
执行 LockSupport.park()
TIME_WAITING:超时等待状态,超时以后自动返回;
执行 sleep(long)
执行 wait(long)、join(long)
执行 LockSupport.parkNanos(long)、LockSupport.parkUntil(long)
TERMINATED:终止状态,表示当前线程执行完毕 。
3|2Java 线程状态转换
在这借用一下大佬的图,因为这张图画真的很棒:
4|0 总结一下
4|1Java 线程的状态:
操作系统层面:
有 5 个状态,分别是:New(新建)、Runnable(就绪)、Running(运行)、Blocked(阻塞)、Dead(死亡)。
JVM 层面:
有 6 个状态,分别是:NEW(新建)、RUNNABLE(运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(超时等待)、TERMINATED(终止)。
4|2Java 线程的状态控制:
主要由这几个方法来控制:sleep、join、yield、wait、notify 以及 notifyALL。
评论