前言
线程的使用想必大家都很熟悉,那么关于如何停止一个线程,或者说如何正确的停止一个线程,尤其是在涉及到锁、多个线程需要交互的情况,应该如何去停止线程来保证程序的行为符合自己的预期呢?
停止线程,我觉得首先需要明确自己想要停止线程的'停止'到底需要线程干什么。是不再需要线程执行任务了,让线程直接TERMINATED
,还是只是需要线程暂时挂起一段时间,比如WAITING
或TIMED_WAITING
,等符合条件之后再被唤醒继续执行。如果不太了解TERMINATED
、WAITING
或TIMED_WAITING
指的是什么,可以先看看Java线程的状态这篇文章。
- 在这讨论的是如何让线程终止自己的任务,并且能够正确的释放资源、不会对与其交互的线程产生预期之外的影响
线程停止的方法
Java中的Thread提供了停止线程和挂起线程的方法,但是它们都被废弃了
Deprecated: This method was originally designed to force a thread to stop and throw a
ThreadDeath
as an exception. It was inherently unsafe. Stopping a thread with Thread.stop causes it to unlock all of the monitors that it has locked (as a natural consequence of the uncheckedThreadDeath
exception propagating up the stack). If any of the objects previously protected by these monitors were in an inconsistent state, the damaged objects become visible to other threads, potentially resulting in arbitrary behavior. Many uses ofstop
should be replaced by code that simply modifies some variable to indicate that the target thread should stop running. The target thread should check this variable regularly, and return from its run method in an orderly fashion if the variable indicates that it is to stop running. If the target thread waits for long periods (on a condition variable, for example), theinterrupt
method should be used to interrupt the wait. For more information, see Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?
从官方文档的描述中我们可以看到,
stop
本身是不安全的。当调用stop
方法去停止线程时,线程会立即释放它持有的所有监视器(monitors)锁,受监视器锁住的对象对其它线程就会变得可见。由于执行这个停止操作是随时的,不能保证操作的原子性,线程在执行某些任务的中途就退出了,比如写入操作,写到一半就直接被停止了。那么被写入的对象就会处于不一致的状态。此时该线程就留下了一个烂摊子
,但是它已经停止了,没有人再去处理这个烂摊子
了。这个时候有另外一个线程再去访问该对象,就可能会出现预期之外的结果。另外停止的时候,对于IO操作,停止的时候也没有机会去关闭流。所以不能直接使用stop()
停止一个线程
Deprecated: This method was designed to suspend the Thread but it was inherently deadlock-prone. If the target thread holds a lock on the monitor protecting a critical system resource when it is suspended, no thread can access this resource until the target thread is resumed. If the thread that would resume the target thread attempts to lock this monitor prior to calling
resume
, deadlock results. Such deadlocks typically manifest themselves as "frozen" processes. For more information, see Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?
对于
suspend
方法,挂起线程,配合resumed
方法一起使用。可以达到挂起和恢复线程运行的目的。但是suspend
也被废弃了。这是为什么呢?首先第一点,如果使用不当,先执行了resumed
,才suspend
。那么线程就被一直挂起了。第二,suspend
的线程不会释放持有的锁。如果线程A持有锁lockA
,然后被suspend
,此时线程B要resumed
线程A,但是在resumed
之前需要获取锁lockA
,这就会导致死锁。这么容易导致出现问题,suspend
被废弃也就不难理解了。关于
stop
、suspend
被废弃的更多信息可以看 Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?
线程停止的方法
- 既然线程停止的方法都被废弃了,那么应该如何让线程停止呢?实际上在
stop
方法的注释中给我们提到了
Many uses of
stop
should be replaced by code that simply modifies some variable to indicate that the target thread should stop running. The target thread should check this variable regularly, and return from its run method in an orderly fashion if the variable indicates that it is to stop running. If the target thread waits for long periods (on a condition variable, for example), theinterrupt
method should be used to interrupt the wait.
简单翻译下就是可以通过修改一些变量以指示目标线程应该停止运行,目标线程应该定期检查这个变量,如果该变量表示它将停止运行,则以有序的方式从其run方法返回。如果目标线程等待很长时间(例如,在一个条件变量上),应该使用interrupt
方法来中断等待。
Unless the current thread is interrupting itself, which is always permitted, the checkAccess method of this thread is invoked, which may cause a SecurityException to be thrown.
If this thread is blocked in an invocation of the Object#wait(), Object#wait(long), or Object#wait(long, int) methods of the Object class, or of the join(), join(long), join(long,int), sleep(long), or sleep(long,int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException.
If this thread is blocked in an I/O operation upon an then the channel will be closed, the thread's interrupt status will be set, and the thread will receive a .
If this thread is blocked in a java.nio.channels.Selector then the thread's interrupt status will be set and it will return immediately from the selection operation, possibly with a non-zero value, just as if the selector's wakeup() method were invoked.
If none of the previous conditions hold then this thread's interrupt status will be set.
Interrupting a thread that is not alive need not have any effect.
Java线程提供了中断方法interrupt()
,当然它不是真正的打断线程的运行,它是native方法,原理就是利用一个标记记录线程的中断状态,也就是记录线程有没有被其它线程执行了中断操作。调用它仅仅只是为线程打了一个中断的标记。而线程可以通过静态方法Thread.interrupted()
或成员方法isInterrupted()
来感知其它线程对自己的中断操作从而作出相应的响应
注释中也说明了,当线程处于WAITING
,TIMED_WAITING
或在进行I/O操作阻塞时,调用interrupt()
会接收到一个InterruptedException
,并且中断状态也会被清除
class ThreadTest {
@Test
fun test() {
val thread = MyThread("thread 1")
thread.start()
thread.interrupt()
println("thread.isInterrupted():${thread.isInterrupted()}")
Thread.sleep(6000)
println("thread state:${thread.getState()}")
println("主线程等待6s结束")
}
class MyThread constructor(name: String) :
Thread(name) {
var count = 0
override fun run() {
super.run()
while (true) {
//do sth
if (isInterrupted()) {
count++
if (count < 5) {
println("中断状态:${isInterrupted()}")
}else{
println("结束线程")
break
}
}
}
}
}
}
- 结果如下
class ThreadTest {
@Test
fun test() {
val thread = MyThread("thread 1")
thread.start()
thread.interrupt()
println("thread.isInterrupted():${thread.isInterrupted()}")
Thread.sleep(6000)
println("thread state:${thread.getState()}")
println("主线程等待6s结束")
}
class MyThread constructor(name: String) :
Thread(name) {
var count = 0
override fun run() {
super.run()
while (true) {
//do sth
try {
sleep(500)
if (isInterrupted()) {
count++
if (count < 5) {
println("中断状态:${isInterrupted()}")
}else{
println("结束线程")
break
}
}
}catch (e:InterruptedException){
println("接收到 InterruptedException,中断状态:${isInterrupted()}")
}
}
}
}
}
-
结果如下
可以看到调用
thread.interrupt()
之后,成员方法isInterrupted()
就可以感知到中断操作,如果是被阻塞,调用时会抛出一个InterruptedException
,中断状态随之清除
isInterrupted()和interrupted()的区别
前面提到成员方法isInterrupted()
和静态方法interrupted()
都能感知到中断操作。那么它们之间有什么区别?
- 先来看下面的例子
class ThreadTest {
@Test
fun test() {
val thread = MyThread("thread 1")
thread.start()
thread.interrupt()
println("thread.isInterrupted():${thread.isInterrupted()}")
Thread.sleep(6000)
println("thread state:${thread.getState()}")
println("主线程等待6s结束")
}
class MyThread constructor(name: String) :
Thread(name) {
var count = 0
override fun run() {
super.run()
while (true) {
//do sth
count++
if (count < 5) {
println("中断状态 isInterrupted():${isInterrupted()}")
println("中断状态 interrupted():${Thread.interrupted()}")
} else {
println("结束线程")
break
}
}
}
}
}
-
结果如下
乍一看好像也没有区别,输出的结果都是一致的,实际上这也正是它们的区别导致的。通过前面的例子,我们可以发现,执行了中断操作后,多次调用isInterrupted()
的返回结果一直返回true
,当然抛InterruptedException
之后由于中断状态被清除,isInterrupted()
的返回结果为false
。而执行了中断操作后,第一次调用interrupted()
的结果为true
,而且调用interrupted()
也会清除中断状态,所以之后的中断状态一直为false
,只有再次执行中断操作,才会返回true
在了解了两者的区别之后,针对不同的需求才能更好的选择使用哪个方法来监听中断状态
打中断标记
官方提供的中断方法是native的,我们知道jni调用多多少少还是有一点性能上的消耗的。所以我们可以自己给线程定义一个中断标记
class ThreadTest {
@Test
fun test() {
val thread = MyThread("thread 1")
thread.start()
Thread.sleep(2)
thread.stop = true
Thread.sleep(6000)
println("thread state:${thread.getState()}")
println("主线程等待6s结束")
}
class MyThread constructor(name: String) :
Thread(name) {
//volatile保证可见性
@Volatile var stop = false
override fun run() {
super.run()
while (true) {
if (stop){
break
}
println("do sth")
}
println("线程结束")
}
}
}
- 对于系统提供的方法和自己打标记的方法又应该如何选择呢?实际上可以看到系统提供的
interrupt()
方法对于长时间挂起的线程,比如被Object.wait
、Thread.sleep
等方法阻塞住的线程也可以正常的停止。而如果是自己打标记的,如果线程被阻塞了,就无法正常响应达到停止的目的了。所以如果想要支持阻塞也能响应,就应选择系统提供的interrupt()
方法,如果不需要支持,则更推荐使用自己打中断标记,从性能上来说会更加好一点。