如何正确停止Java线程,终止Java线程的三种方法
在 Java 中有以下 3 种方法可以终止正在运行的线程:
- 使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程终止。
- 使用 stop() 方法强行终止线程,但不推荐,该方法已被弃用,原因见后文。
- 使用 interrupt 方法中断线程。
以下内容翻译自 JDK1.5官方文档 ,内容有微调。未发现JDK1.8对应文档与JDK1.5的内容有明显不同。
停止一个线程的推荐做法
stop
的大多数用法应由简单地修改某些变量以指示目标线程应停止运行的代码代替。 目标线程应定期检查此变量,如果该变量指示要停止运行,则应有序地从其运行方法返回。 (这是Java始终推荐的方法)。为了确保对stop-request
进行及时的通信,变量必须是volatile
(或必须同步访问变量)。
例如,假设您的程序包含以下start
,stop
和run
方法:
private Thread blinker;
public void start() {
blinker = new Thread(this);
blinker.start();
}
public void stop() {
blinker.stop(); // UNSAFE!
}
public void run() {
Thread thisThread = Thread.currentThread();
while (true) {
try {
thisThread.sleep(interval);
} catch (InterruptedException e){
}
repaint();
}
}
可以通过将程序的stop和run方法替换为下面的代码来避免使用Thread.stop
:
private volatile Thread blinker;
public void stop() {
blinker = null;
}
public void run() {
Thread thisThread = Thread.currentThread();
while (blinker == thisThread) {
try {
thisThread.sleep(interval);
} catch (InterruptedException e){
}
repaint();
}
}
=== 后面的内容可以不看,看的话务必从头到尾按顺序阅读 ===
为什么 Thread.stop
被废弃?
因为它本质上是不安全的。 _停止线程会使它解锁它已锁定的所有监视器_。 (当ThreadDeath
异常在堆栈中向上传播时,监视器将被解锁。)如果先前由这些监视器保护的任何对象处于不一致状态,则其他线程现在可能会以不一致状态查看这些对象。 这样的对象被称为_已损坏的对象_。 当线程对损坏的对象进行操作时,可能会导致任意行为。 此行为可能是微妙的,难以检测,或者可能是明显的。 与其他未检查的异常不同,ThreadDeath
会无声地杀死线程。 因此,用户没有警告其程序可能已损坏。 在实际损坏发生后的任何时间,腐败会体现出来(注:corruption,意为腐败,类似代码的bad smell
,指程序中出现的问题或bug)。
不能只是捕捉ThreadDeath
异常并修复损坏的对象吗?
从理论上讲,也许可以,但这会_使编写正确的多线程代码的任务大大复杂化_。该任务几乎是无法克服的,原因有两个:
- 线程_几乎可以在任何地方_引发
ThreadDeath
异常 。考虑到这一点,必须仔细研究所有同步的方法和块。 ThreadDeath
从第一个(在catch
orfinally
子句中)清除时,线程可以引发第二个异常。必须重复进行清理,直到成功。确保该代码很复杂。
如何停止等待较长时间的线程(例如,等待输入)?
这就是Thread.interrupt
方法的用途。 可以使用上面展示的相同的“基于状态”的信令机制,但是状态更改(在前面的示例中,_blinker = null_)之后可以调用Thread.interrupt
来中断等待:
public void stop() {
Thread moribund = waiter;
waiter = null;
moribund.interrupt();
}
为了使该技术起作用,至关重要的是,任何捕获中断异常并且不准备处理中断异常的方法都必须立即重新声明该异常。 我们说重新声明而不是重新抛出,因为并非总是可能重新抛出异常。 如果未声明捕获InterruptedException
的方法引发此(checked)Exception,则它应使用下面的提示“重新中断自身”:
Thread.currentThread().interrupt();
这样可以确保线程尽快引发InterruptedException
。
如果线程不响应Thread.interrupt
怎么办?
在某些情况下,您可以使用特定于应用程序的技巧。 例如,如果某个线程正在一个已知的套接字上等待,则可以关闭该套接字以使该线程立即返回。 不幸的是,一般来说,真的没有什么技术能奏效。 应该注意的是,在所有等待线程不响应Thread.interrupt
的情况下,它也不响应Thread.stop
。 这样的情况包括故意的拒绝服务攻击,以及Thread.stop
和Thread.interrupt
无法正常工作的I / O操作。(_注释_:这段的个人理解,如果程序不响应Thread.interrupt
,那就没办法了,自己去想别的方法解决吧。所以还是尽量使用推荐的方法来控制线程停止与运行)
为什么不赞成使用Thread.suspend
和Thread.resume
?
Thread.suspend
本质上容易死锁。 如果目标线程在挂起时,在监视器上持有一个保护关键系统资源的锁,则在恢复目标线程之前,没有线程可以访问该资源。 如果将恢复目标线程的线程在调用resume之前尝试锁定此监视器,则会导致死锁。 这种僵局通常表现为“冻结”进程。
应该使用什么代替Thread.suspend
和Thread.resume
?
与Thread.stop
一样,谨慎的方法是让“目标线程”轮询一个指示线程所需状态(活动或挂起)的变量。 当所需的状态被挂起时,线程使用Object.wait
等待。 恢复线程后,将使用Object.notify
通知目标线程。
例如,假设您的程序包含以下“鼠标按下”事件处理程序,该事件处理程序的功能是可以切换blinker
线程的状态:
private boolean threadSuspended;
Public void mousePressed(MouseEvent e) {
e.consume();
if (threadSuspended)
blinker.resume();
else
blinker.suspend(); // DEADLOCK-PRONE!容易出现死锁
threadSuspended = !threadSuspended;
}
您可以通过将上面的事件处理程序替换为下面的代码来避免使用Thread.suspend
和Thread.resume
:
public synchronized void mousePressed(MouseEvent e) {
e.consume();
threadSuspended = !threadSuspended;
if (!threadSuspended)
notify();
}
并添加下面的代码到 "run loop":
synchronized(this) {
while (threadSuspended)
wait();
}
wait方法抛出InterruptedException
,因此它必须在try ... catch子句中。 可以将其与sleep放在同一个的子句中。 该检查应在sleep之后(而不是在sleep之前),以便在线程“resumed”时立即重新绘制窗口。 生成的run
方法如下:
public void run() {
while (true) {
try {
Thread.currentThread().sleep(interval);
synchronized(this) {
while (threadSuspended)
wait();
}
} catch (InterruptedException e){
}
repaint();
}
}
请注意,“鼠标按下”方法中的notify和run方法中的wait在同步块(synchronized)内部。这种做法是编程语言要求的,并确保wait和notify正确地按顺序执行。 实际上,这消除了可能导致“已暂停”线程错过通知并无限期保持暂停的竞争条件。
随着平台的成熟,尽管Java同步(synchronized)的性能开销在降低,但它永远不会“免费”(注:免费指几乎可以忽略的性能开销)。 一个简单的技巧可以用来删除我们添加到“运行循环”的每个迭代中的同步。 所添加的同步块被稍微复杂一点的代码所代替,仅当线程实际上已被挂起时才进入同步块:
if (threadSuspended) {
synchronized(this) {
while (threadSuspended)
wait();
}
}
在没有显式同步的情况下,必须将threadSuspended
设置为volatile
,以确保及时传达suspend-request。
最终生成的run方法为:
private boolean volatile threadSuspended;
public void run() {
while (true) {
try {
Thread.currentThread().sleep(interval);
if (threadSuspended) {
synchronized(this) {
while (threadSuspended)
wait();
}
}
} catch (InterruptedException e){
}
repaint();
}
}
可以结合两种技术来产生可以安全地“停止”或“暂停”的线程吗?
可以的, 这相当简单。 一个微妙之处是目标线程可能在另一个线程试图将其停止时已被挂起。 如果stop方法仅将状态变量(blinker)设置为null,则目标线程将保持挂起状态(在监视器上等待),而不是应有的退出。 如果重新启动了applet,则多个线程可能最终会同时在监视器上等待,从而导致行为不稳定。
要纠正这种情况,stop方法必须确保目标线程在挂起后立即恢复。 目标线程恢复后,必须立即识别出它已停止,并正常退出。 这是生成的run和stop方法的外观:
public void run() {
Thread thisThread = Thread.currentThread();
while (blinker == thisThread) {
try {
thisThread.sleep(interval);
synchronized(this) {
while (threadSuspended && blinker==thisThread)
wait();
}
} catch (InterruptedException e){
}
repaint();
}
}
public synchronized void stop() {
blinker = null;
notify();
}
如上所述,如果stop方法调用Thread.interrupt
,它也不必调用notify,但仍必须同步。 这样可以确保目标线程不会由于竞争条件而错过中断。
关于 Thread.destroy
方法
Thread.destroy
从未真正被实现。 如果实现了它,使用Thread.suspend
容易发生死锁。 (实际上,它与Thread.suspend
大致等效,没有后续的Thread.resume
的可能性。)我们目前不实现它,但也不会弃用它(将来将阻止其实现)。 尽管肯定会发生死锁,但有人认为,在某些情况下,某个程序愿意冒着deadlock的风险也不想直接退出。
Java8中的destroy
方法源码:
/**
* Throws {@link NoSuchMethodError}.
* 此方法最初设计为在不进行任何清理的情况下销毁此线程。
* 它所持有的任何监视器都将保持锁定状态。
* 但是,该方法从未实现。如果要实现,则将以{@link #suspend}的方式发生死锁。
* 如果目标线程在被销毁时持有一个保护关键系统资源的锁,则没有线程可以再次访问该资源。
* 如果另一个线程曾尝试锁定此资源,则会导致死锁。这种死锁通常表现为“冻结”进程。
*/
@Deprecated
public void destroy() {
throw new NoSuchMethodError();
}