在Java中停止循环线程

7
我正在使用一个不断从队列中读取的线程。
类似这样:
public void run() {
    Object obj;
    while(true) {
        synchronized(objectsQueue) {
            if(objectesQueue.isEmpty()) {
                try {
                    objectesQueue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                obj = objectesQueue.poll();
            }
        }

        // Do something with the Object obj
    }
}

如何停止该线程?

我看到有两个选项:

1 - 由于Thread.stop()已被弃用,我可以实现一个stopThisThread()方法,使用原子检查条件变量。

2 - 向队列发送死亡事件对象或类似的东西。当线程获取死亡事件时,它将退出。

我更喜欢第一种方式,但我不知道何时调用stopThisThread()方法,因为可能有一些内容正在前往队列,而停止信号可能会先到达(这是不可取的)。

有什么建议吗?


3
请注意,你的wait应该在一个while循环中,而不是在if语句中,以防止虚假唤醒。 - Tom Hawtin - tackline
对于那些依赖中断的人:只想说生活从来不是那么简单,有时候会在一个不可中断的“活动”上调用中断,而该活动完全不会注意到中断。这是通过经验得出的结论。尽管如此,我还是不知道更好的方法...请仔细查看API:http://java.sun.com/javase/6/docs/api/index.html?java/lang/Thread.html,查看interrupt()方法。 - Yaneeve
@halfwarp:对Stephen C的回答点赞。你所说的“死亡事件”实际上被称为“毒丸”:你只需将一个特殊对象(毒丸)入队,表示“停止线程”,一旦你出队它。这样,在毒丸到达之前,“可能正在前往队列”的任何内容都会被处理。 - SyntaxT3rr0r
@Yaneeve:这就是为什么你必须在所有处理中断的代码中调用isInterrupted()。但这并不意味着使用它是错误和/或不可能的。事实上,这是唯一的方法来告诉一个正在阻塞I/O调用的线程检查某些条件。 - Joachim Sauer
@Joachim:正如我所说,有时候你就是没有那个特权。如果像我这种情况,被中断的代码是由第三方库运行的,并且该库不“感觉”检查中断状态,那么事情就会变得有点复杂。更明确地说,我曾经遇到过一个问题,即取消一个尚未超时的CORBA请求。 - Yaneeve
6个回答

6
< p > "DeathEvent"(或者通常称为“毒丸”)方法在需要在关闭前完成队列中所有工作时效果很好。问题是这可能需要很长时间。< /p> < p > 如果您想尽快停止,请按照以下建议操作:< /p>
BlockingQueue<O> queue = ...

...

public void run() {
   try {
       // The following test is necessary to get fast interrupts.  If
       // it is replaced with 'true', the queue will be drained before
       // the interrupt is noticed.  (Thanks Tim)
       while (!Thread.interrupted()) {
           O obj = queue.take();
           doSomething(obj);
       }
   } catch (InterruptedException ex) {
       // We are done.
   }
}

要停止使用run方法实例化的线程t,只需调用t.interrupt();
如果将上述代码与其他答案进行比较,您会注意到使用BlockingQueueThread.interrupt()简化了解决方案。
我还要声明,额外的stop标记是不必要的,并且从大局来看可能是有害的。一个行为良好的工作线程应该尊重中断。意外的中断仅意味着工作线程正在以原始程序员没有预料到的上下文中运行。最好的情况是,工作线程按照指示执行...也就是说,它应该停止...无论这是否符合原始程序员的概念。

@Stephen C: +1... 此外,在JCIP中建议,如果您收到意外中断,则应在finally块中使用*if(interrupted){ Thread.currenThread().interrupt() )}结构恢复中断状态。 嗯,所以毕竟也许更好的方法是在收到意外中断并恢复中断状态后同时退出,然后再使用一些stopThisThread()*方法来实现“干净”的停止,对吧? - SyntaxT3rr0r
@WizardOfOdds - 我不明白你的意思。1)在run方法中调用interrupt()来恢复标志位是没有意义的,因为调用它的线程即将死亡。2)你刚才说的并没有解释为什么通过中断停止是不好的,或者为什么“干净”的停止机制更好。(而JCIP的建议并不是一个解释。) - Stephen C
@WizardOfOdds - 不管JCIP可能会说什么,显然它在这种情况下并不适用。 - Stephen C
1
@Stephen C:你应该在 while 循环的条件语句中检查中断标志。根据 javadoc 的说明,BlockingQueue 实现只有在等待时才必须抛出 InterruptedException。如果队列非空,它不必等待。严格遵守 javadoc,你的 run 方法不能保证快速退出...实际上可能根本不会退出。 - Tim Bender
@Stephen C,我想澄清一下。只要队列不为空,就可能不会抛出InterruptedException异常。如果你查看PriorityBlockingQueue和SynchronousQueue的实现,你会发现它们在执行可中断的阻塞操作之前,都允许从take()方法返回一个值。 - Tim Bender
显示剩余2条评论

1
在您的读取线程中,有一个布尔变量stop。当您希望该线程停止时,请将其设置为true并中断该线程。在读取线程中,当安全时(即没有未处理的对象时),检查停止变量的状态,并根据下面的内容退出循环。
public class readerThread extends Thread{
    private volitile boolean stop = false;
    public void stopSoon(){
        stop = true;
        this.interrupt();
    }
    public void run() {
        Object obj;
        while(true) {
            if(stop){
                return;
            }
            synchronized(objectsQueue) {
            if(objectesQueue.isEmpty()) {
                try {
                    objectesQueue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(stop){
                    return;
                }    
                obj = objectesQueue.poll();
                // Do something with the Object obj
            }
        }
    }


}
public class OtherClass{
     ThreadReader reader;
     private void start(){
          reader = ...;
          reader.start();
     }

     private void stop(){
          reader.stopSoon();
          reader.join();     // Wait for thread to stop if nessasery.
     }
}

这是我经典编写队列工作者的方式。这种方法与被接受的答案之间的显著区别在于,“毒丸”方法确保在退出之前完全处理队列,而不是放弃它。哪种方法更正确取决于问题空间。您还可以考虑在interrupt()之后添加join(),这使stopSoon()阻塞直到线程实际死亡。 - nsayer

1
为什么不使用可以在需要时停止的调度程序?标准调度程序支持重复调度,还会等待工作线程完成后再安排新的运行。
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
service.scheduleWithFixedDelay(myThread, 1, 10, TimeUnit.SECONDS);

这个示例将以10秒的延迟运行您的线程,这意味着当一个运行结束时,它会在10秒后重新启动。而且,您无需重新发明轮子,就能得到这个功能。

service.shutdown()

现在不再需要使用 while(true)。

ScheduledExecutorService Javadoc


1
service.shutdown()与毒丸方法类似,意味着不再接受更多任务,正在执行的任务将继续运行,并且先前接受的任务将被执行。相反,service.shutdownNow()将尝试停止正在执行的任务,并且之前接受的任务将不会被执行。 - user454322

0

我认为你的两种情况实际上展示了相同的潜在行为。对于第二种情况,考虑线程A添加DeathEvent之后,线程B添加FooEvent。当你的工作线程接收到DeathEvent时,它后面仍然有一个FooEvent,这与你在选项1中描述的情况相同,除非你在返回之前尝试清除队列,但那样你实际上是在保持线程活动,而你想要做的是停止它。

我同意你的第一种选择更可取。一个潜在的解决方案取决于你的队列如何填充。如果它是你的工作线程类的一部分,你可以让你的stopThisThread()方法设置一个标志,从入队调用中返回适当的值(或抛出异常),即:

MyThread extends Thread{
  boolean running = true;

  public void run(){
    while(running){
      try{
        //process queue...
      }catch(InterruptedExcpetion e){
        ...
      }
    }
  }

  public void stopThisThread(){
    running = false;
    interrupt();
  }

  public boolean enqueue(Object o){
    if(!running){
       return false;
         OR
       throw new ThreadNotRunningException();
    }
    queue.add(o);
    return true;
  }
}

然后, 尝试将事件入队的对象将负责适当处理它,但至少它将知道事件不在队列中,因此不会被处理。


除非您将运行变量定义为volatile,否则您的方法是错误的。 此外,不建议扩展线程,而应实现Callable或Runnable接口,这样您就不会在作用域中拥有中断功能。 - Roman

0
通常我会在包含线程的类中设置一个标志,在我的线程代码中使用它。 (注意:我使用while(flag)而不是while(true))
然后在该类中创建一个方法来将标志设置为false;
private volatile bool flag = true;

public void stopThread()
{
   flag = false;
}

    public void run() {
        Object obj;
        while(flag) {
            synchronized(objectsQueue) {
                if(objectesQueue.isEmpty()) {
                    try {
                        objectesQueue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    obj = objectesQueue.poll();
                }
            }

            // Do something with the Object obj
        }
    }

我觉得这看起来不是线程安全的。flag变量在不同的线程中被读取和写入,但没有进行适当的同步。 - Stephen C
2
@Romain Hippeau:我将你的布尔标志volatile,否则,正如Stephen C指出的那样,你的代码不会被正确同步。 - SyntaxT3rr0r

0

方法1是首选。

只需将一个易失性的stop字段设置为true,并在运行线程上调用interrupt()。这将强制任何等待的I/O方法返回一个InterruptedException(如果您的库编写正确,这将被优雅地处理)。


2
能否给出负评者针对这个答案的问题意见? - Joachim Sauer
1
我刚刚进行了反对投票。我的理由是使用单独的停用词字段是不必要的。并发是一个困难的话题。虽然你的回答在技术上没有问题,但它也不符合最佳实践,我认为限制那些虽然可行但不符合最佳实践的答案对社区有利。 - Tim Bender
@TimBender 我恭敬地不同意。在队列工作器对象上使用stop()方法,它仅设置一个布尔字段并中断线程,根据任何社区共识都是“最佳实践”。 - nsayer
@nsayer,很多人随意穿过街道,这并不意味着这样做是最佳实践,而不是使用人行横道。很多人使用额外的布尔值,这并不意味着这样做是最佳实践,而不是使用线程的中断状态。 - Tim Bender

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接