如何设置线程超时时间

283

我想要在一定的时间内运行一个线程,如果它没有在这段时间内完成,我想要杀死它、抛出一些异常或以某种方式处理它。如何实现?

这个帖子中,我发现一种方法是在线程的run()方法中使用TimerTask。

是否有更好的解决方案?

 
编辑:我需要更清晰的答案,因此添加了悬赏。下面给出的ExecutorService代码并没有解决我的问题。为什么在执行(某些代码 - 我无法控制此代码)后要休眠()?如果代码已经完成,而sleep()被中断,那怎么可能会超时呢?

需要执行的任务不在我的控制之下,它可以是任何代码片段。问题是这个代码片段可能会陷入无限循环。我不希望发生这种情况。因此,我只想在单独的线程中运行该任务。父线程必须等到该线程完成,并需要知道任务的状态(即是否超时、是否发生异常或者是否成功)。如果任务进入无限循环,我的父线程将一直无限期地等待,这并不是理想的情况。


下面给出的ExecutorService代码并没有解决我的问题。为什么在执行完代码后我需要调用 sleep()方法?如果代码已经完成并且sleep()被中断,那怎么能算作timeout呢? - java_geek
7
那个 sleep() 只是一个代表“长时间运行任务”的样板。请将其替换为您要运行的真实任务 ;) - BalusC
1
“长时间运行的任务”恰好响应其线程上的interrupt()调用……并非所有“阻塞”调用都会响应,正如我在回答中所指出的那样。您尝试中止的任务的具体情况对应用的方法有很大影响。有关任务的更多信息将会有所帮助。 - erickson
如果这些答案没有解决问题,那么我猜更多的细节/代码应该能够帮助回答。 - Elister
你想要限制时间的这些线程,它们是在进行阻塞调用,还是在某个循环中,你可以轻松地检查一些变量来看是否到了退出的时间? - Scott Smith
@java_geek:现在答案更清晰了吗? - BalusC
18个回答

401

实际上,建议使用ExecutorService而不是Timer,下面是一个SSCCE示例:

package com.stackoverflow.q2275443;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class Test {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(new Task());

        try {
            System.out.println("Started..");
            System.out.println(future.get(3, TimeUnit.SECONDS));
            System.out.println("Finished!");
        } catch (TimeoutException e) {
            future.cancel(true);
            System.out.println("Terminated!");
        }

        executor.shutdownNow();
    }
}

class Task implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep(4000); // Just to demo a long running task of 4 seconds.
        return "Ready!";
    }
}

尝试使用Future#get()方法中的timeout参数,例如将其增加到5,您将看到线程完成。您可以在catch (TimeoutException e)块中拦截超时。

更新:为了澄清一个概念上的误解,sleep()不必要的。它只是用于SSCCE /演示目的。只需在sleep()的位置上执行您的长时间运行的任务即可。在您的长时间运行的任务内部,应检查线程是否未被中断,如下所示:

while (!Thread.interrupted()) {
    // Do your long running task here.
}

32
Thread.sleep(4000) 替换为其他长时间运行的语句,该示例将不起作用。换句话说,只有当 Task 被设计为了解 Thread.isInterrupted() 状态变化时,此示例才会起作用。 - yegor256
1
有n个人已经评论了这个包的名称,这里又来一个+1。这是一项非常好的技能。谢谢! - ATP
@BalusC 我有一个疑问,Future是否会同步执行,如果它需要超过预定时间,则会被终止。否则,它将在未来某个时候执行,同时我们正在计时...谢谢 - Adeel Ahmad
我有一个使用情况,需要在一个数据源耗时的情况下切换到另一个数据源。您会推荐这种方法吗? 我的疑问是,如果方法执行在超时限制之前甚至没有开始,该怎么办?我对并行与异步方法执行感到困惑。 谢谢 - Adeel Ahmad
例如,我们可以异步执行一个方法,即使它是并行执行,我们也不关心响应何时到达,但我们不想等待它,也不关心方法何时开始执行。但在我的情况下,这非常重要... - Adeel Ahmad
显示剩余5条评论

54

对于任何旧任务都没有一种100%可靠的方法来实现此功能。任务必须考虑到这种能力而编写。

ExecutorService这样的核心Java库会在工作线程上使用interrupt()调用来取消异步任务。因此,例如,如果任务包含某种循环,您应该在每次迭代时检查其中断状态。如果任务正在执行I/O操作,则它们也应当是可中断的——设置它们可能会有些棘手。无论如何,请记住,代码必须积极地检查中断;设置中断并不一定有效果。

当然,如果您的任务只是一些简单的循环,您可以在每次迭代时检查当前时间,并在经过指定的超时时间后放弃。在这种情况下,不需要使用工作线程。


根据我的经验,唯一不响应中断的代码是在本机代码中阻塞(等待操作系统)。 - Thorbjørn Ravn Andersen
@ThorbjørnRavnAndersen 我同意,但那是很多代码。我的观点是没有通用的机制来处理这个问题;你必须理解任务的中断策略。 - erickson
@erickson,我同意你的直截了当的回答。每个任务都必须定义一个取消策略,如果你想在中途停止它。或者线程应该知道当它被中断时应该做什么。毕竟,中断和停止任何线程只是一个请求,目标线程可能接受或拒绝,所以最好编写任务时要考虑到这一点。 - AKS
执行器服务不能选择在调用线程上运行任务吗?此外,执行器服务可以选择在将来的某个时间执行任务吗? - filthy_wizard
@user1232726 父接口 Executorexecute() 方法可能在调用线程中运行任务。对于返回 Future 实例的 ExecutorServicesubmit() 方法没有类似的语句。该服务的含义是有工作线程必须通过关闭进行清理,并且任务是异步执行的。也就是说,合同中没有规定 ExecutorService 不能在提交线程中执行任务;这些保证来自于实现 API,例如 Executors 工厂。 - erickson

13
考虑使用一个 ExecutorService 实例,其中 invokeAll()invokeAny() 方法都带有 timeout 参数。
当前线程将阻塞,直到方法完成(不确定是否希望如此),无论是因为任务正常完成还是达到了超时时间。您可以检查返回的 Future 对象以确定发生了什么。

如果阻塞当前线程不可取,那么有哪些选项呢? - DanielM

10
假设您无法控制线程代码:
从上面提到的Java documentation中:

如果线程不响应Thread.interrupt会怎样?

在某些情况下,您可以使用特定于应用程序的技巧。例如, 如果线程正在等待已知套接字,则可以关闭套接字以使线程立即返回。不幸的是,确实没有通用的技术可行。 值得注意的是,在所有等待线程不响应Thread.interrupt的情况下,它也不会响应Thread.stop。 这种情况包括故意的拒绝服务攻击和 Thread.stop 和 thread.interrupt 无法正常工作的 I/O 操作。

最后: 确保所有线程都可以中断,否则您需要具有线程的特定知识-例如设置标志。您还可以要求将任务与停止它所需的代码一起提供给您-定义一个带有stop()方法的接口。当您无法停止任务时,也可以发出警告。

8

BalusC说:

更新:为了澄清一个概念上的误解,sleep()方法并不是必需的,它只是用于SSCCE /演示目的。在那里,只需在sleep()方法所在的位置替换成您的长时间运行任务即可。

但是,如果您将 Thread.sleep(4000); 替换为 for (int i = 0; i < 5E8; i++) {},则不会编译通过,因为空循环不会抛出 InterruptedException 异常。

而要使线程可中断,它必须抛出 InterruptedException 异常。

这对我来说似乎是一个严重的问题。我看不出如何使此答案适用于一般的长时间运行任务。

编辑以添加:我将其重新提问为一个新问题:[ 在固定时间后中断线程,它必须抛出InterruptedException吗? ]


我处理这个问题的方式是在公共的Class<T> call {}方法中添加“throws Exception”。 - Roberto Linares

6
我之前创建了一个助手类专门针对这个问题。非常好用:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
 * TimeOut class - used for stopping a thread that is taking too long
 * @author Peter Goransson
 *
 */
public class TimeOut {

    Thread interrupter;
    Thread target;
    long timeout;
    boolean success;
    boolean forceStop;

    CyclicBarrier barrier;

    /**
     * 
     * @param target The Runnable target to be executed
     * @param timeout The time in milliseconds before target will be interrupted or stopped
     * @param forceStop If true, will Thread.stop() this target instead of just interrupt() 
     */
    public TimeOut(Runnable target, long timeout, boolean forceStop) {      
        this.timeout = timeout;
        this.forceStop = forceStop;

        this.target = new Thread(target);       
        this.interrupter = new Thread(new Interrupter());

        barrier = new CyclicBarrier(2); // There will always be just 2 threads waiting on this barrier
    }

    public boolean execute() throws InterruptedException {  

        // Start target and interrupter
        target.start();
        interrupter.start();

        // Wait for target to finish or be interrupted by interrupter
        target.join();  

        interrupter.interrupt(); // stop the interrupter    
        try {
            barrier.await(); // Need to wait on this barrier to make sure status is set
        } catch (BrokenBarrierException e) {
            // Something horrible happened, assume we failed
            success = false;
        } 

        return success; // status is set in the Interrupter inner class
    }

    private class Interrupter implements Runnable {

        Interrupter() {}

        public void run() {
            try {
                Thread.sleep(timeout); // Wait for timeout period and then kill this target
                if (forceStop) {
                  target.stop(); // Need to use stop instead of interrupt since we're trying to kill this thread
                }
                else {
                    target.interrupt(); // Gracefully interrupt the waiting thread
                }
                System.out.println("done");             
                success = false;
            } catch (InterruptedException e) {
                success = true;
            }


            try {
                barrier.await(); // Need to wait on this barrier
            } catch (InterruptedException e) {
                // If the Child and Interrupter finish at the exact same millisecond we'll get here
                // In this weird case assume it failed
                success = false;                
            } 
            catch (BrokenBarrierException e) {
                // Something horrible happened, assume we failed
                success = false;
            }

        }

    }
}

它被称为这个名字:

long timeout = 10000; // number of milliseconds before timeout
TimeOut t = new TimeOut(new PhotoProcessor(filePath, params), timeout, true);
try {                       
  boolean sucess = t.execute(); // Will return false if this times out
  if (!sucess) {
    // This thread timed out
  }
  else {
    // This thread ran completely and did not timeout
  }
} catch (InterruptedException e) {}  

5
我认为你应该看一下适当的并发处理机制(线程陷入无限循环本身就不好,顺便说一句)。确保你阅读一些关于“杀死}或{{link1:停止线程”的主题。
你所描述的听起来非常像一个“约会”,所以你可能想要看一下CyclicBarrier
可能还有其他构造(例如使用CountDownLatch),可以解决你的问题(一个线程等待超时的闩锁,另一个线程在完成工作后计数器减少闩锁,这将在超时后或启动闩锁倒计时后释放你的第一个线程)。
我通常会推荐两本与此领域相关的书籍:Java并发编程实战Java并发编程的艺术

4
BalusC提供的解决方案中,主线程会在超时期间保持阻塞状态。如果您有一个线程池,并且线程池中有多个线程,则需要相同数量的附加线程,这些线程将使用Future.get(long timeout, TimeUnit unit)阻塞调用等待并在超时期间关闭线程。
解决此问题的通用方法是创建一个ThreadPoolExecutor装饰器,可以添加超时功能。该装饰器类应创建与ThreadPoolExecutor一样多的线程,并且所有这些线程都应仅用于等待和关闭ThreadPoolExecutor。
通用类的实现如下:
import java.util.List;
import java.util.concurrent.*;

public class TimeoutThreadPoolDecorator extends ThreadPoolExecutor {


    private final ThreadPoolExecutor commandThreadpool;
    private final long timeout;
    private final TimeUnit unit;

    public TimeoutThreadPoolDecorator(ThreadPoolExecutor threadpool,
                                      long timeout,
                                      TimeUnit unit ){
        super(  threadpool.getCorePoolSize(),
                threadpool.getMaximumPoolSize(),
                threadpool.getKeepAliveTime(TimeUnit.MILLISECONDS),
                TimeUnit.MILLISECONDS,
                threadpool.getQueue());

        this.commandThreadpool = threadpool;
        this.timeout=timeout;
        this.unit=unit;
    }

    @Override
    public void execute(Runnable command) {
        super.execute(() -> {
            Future<?> future = commandThreadpool.submit(command);
            try {
                future.get(timeout, unit);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } catch (ExecutionException | TimeoutException e) {
                throw new RejectedExecutionException(e);
            } finally {
                future.cancel(true);
            }
        });
    }

    @Override
    public void setCorePoolSize(int corePoolSize) {
        super.setCorePoolSize(corePoolSize);
        commandThreadpool.setCorePoolSize(corePoolSize);
    }

    @Override
    public void setThreadFactory(ThreadFactory threadFactory) {
        super.setThreadFactory(threadFactory);
        commandThreadpool.setThreadFactory(threadFactory);
    }

    @Override
    public void setMaximumPoolSize(int maximumPoolSize) {
        super.setMaximumPoolSize(maximumPoolSize);
        commandThreadpool.setMaximumPoolSize(maximumPoolSize);
    }

    @Override
    public void setKeepAliveTime(long time, TimeUnit unit) {
        super.setKeepAliveTime(time, unit);
        commandThreadpool.setKeepAliveTime(time, unit);
    }

    @Override
    public void setRejectedExecutionHandler(RejectedExecutionHandler handler) {
        super.setRejectedExecutionHandler(handler);
        commandThreadpool.setRejectedExecutionHandler(handler);
    }

    @Override
    public List<Runnable> shutdownNow() {
        List<Runnable> taskList = super.shutdownNow();
        taskList.addAll(commandThreadpool.shutdownNow());
        return taskList;
    }

    @Override
    public void shutdown() {
        super.shutdown();
        commandThreadpool.shutdown();
    }
}

上述装饰器可以如下使用:

import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Main {

    public static void main(String[] args){

        long timeout = 2000;

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 10, 0, TimeUnit.MILLISECONDS, new SynchronousQueue<>(true));

        threadPool = new TimeoutThreadPoolDecorator( threadPool ,
                timeout,
                TimeUnit.MILLISECONDS);


        threadPool.execute(command(1000));
        threadPool.execute(command(1500));
        threadPool.execute(command(2100));
        threadPool.execute(command(2001));

        while(threadPool.getActiveCount()>0);
        threadPool.shutdown();


    }

    private static Runnable command(int i) {

        return () -> {
            System.out.println("Running Thread:"+Thread.currentThread().getName());
            System.out.println("Starting command with sleep:"+i);
            try {
                Thread.sleep(i);
            } catch (InterruptedException e) {
                System.out.println("Thread "+Thread.currentThread().getName()+" with sleep of "+i+" is Interrupted!!!");
                return;
            }
            System.out.println("Completing Thread "+Thread.currentThread().getName()+" after sleep of "+i);
        };

    }
}

3

我给你贴上一段代码,展示了如何解决问题。 例如,我正在读取一个文件。 您可以将此方法用于其他操作,但需要实现kill()方法以便中断主要操作。

希望对您有所帮助。


import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

/**
 * Main class
 * 
 * @author el
 * 
 */
public class Main {
    /**
     * Thread which perform the task which should be timed out.
     * 
     * @author el
     * 
     */
    public static class MainThread extends Thread {
        /**
         * For example reading a file. File to read.
         */
        final private File fileToRead;
        /**
         * InputStream from the file.
         */
        final private InputStream myInputStream;
        /**
         * Thread for timeout.
         */
        final private TimeOutThread timeOutThread;

        /**
         * true if the thread has not ended.
         */
        boolean isRunning = true;

        /**
         * true if all tasks where done.
         */
        boolean everythingDone = false;

        /**
         * if every thing could not be done, an {@link Exception} may have
         * Happens.
         */
        Throwable endedWithException = null;

        /**
         * Constructor.
         * 
         * @param file
         * @throws FileNotFoundException
         */
        MainThread(File file) throws FileNotFoundException {
            setDaemon(false);
            fileToRead = file;
            // open the file stream.
            myInputStream = new FileInputStream(fileToRead);
            // Instantiate the timeout thread.
            timeOutThread = new TimeOutThread(10000, this);
        }

        /**
         * Used by the {@link TimeOutThread}.
         */
        public void kill() {
            if (isRunning) {
                isRunning = false;
                if (myInputStream != null) {
                    try {
                        // close the stream, it may be the problem.
                        myInputStream.close();
                    } catch (IOException e) {
                        // Not interesting
                        System.out.println(e.toString());
                    }
                }
                synchronized (this) {
                    notify();
                }
            }
        }

        /**
         * The task which should be timed out.
         */
        @Override
        public void run() {
            timeOutThread.start();
            int bytes = 0;
            try {
                // do something
                while (myInputStream.read() >= 0) {
                    // may block the thread.
                    myInputStream.read();
                    bytes++;
                    // simulate a slow stream.
                    synchronized (this) {
                        wait(10);
                    }
                }
                everythingDone = true;
            } catch (IOException e) {
                endedWithException = e;
            } catch (InterruptedException e) {
                endedWithException = e;
            } finally {
                timeOutThread.kill();
                System.out.println("-->read " + bytes + " bytes.");
                isRunning = false;
                synchronized (this) {
                    notifyAll();
                }
            }
        }
    }

    /**
     * Timeout Thread. Kill the main task if necessary.
     * 
     * @author el
     * 
     */
    public static class TimeOutThread extends Thread {
        final long timeout;
        final MainThread controlledObj;

        TimeOutThread(long timeout, MainThread controlledObj) {
            setDaemon(true);
            this.timeout = timeout;
            this.controlledObj = controlledObj;
        }

        boolean isRunning = true;

        /**
         * If we done need the {@link TimeOutThread} thread, we may kill it.
         */
        public void kill() {
            isRunning = false;
            synchronized (this) {
                notify();
            }
        }

        /**
         * 
         */
        @Override
        public void run() {
            long deltaT = 0l;
            try {
                long start = System.currentTimeMillis();
                while (isRunning && deltaT < timeout) {
                    synchronized (this) {
                        wait(Math.max(100, timeout - deltaT));
                    }
                    deltaT = System.currentTimeMillis() - start;
                }
            } catch (InterruptedException e) {
                // If the thread is interrupted,
                // you may not want to kill the main thread,
                // but probably yes.
            } finally {
                isRunning = false;
            }
            controlledObj.kill();
        }
    }

    /**
     * Start the main task and wait for the end.
     * 
     * @param args
     * @throws FileNotFoundException
     */
    public static void main(String[] args) throws FileNotFoundException {
        long start = System.currentTimeMillis();
        MainThread main = new MainThread(new File(args[0]));
        main.start();
        try {
            while (main.isRunning) {
                synchronized (main) {
                    main.wait(1000);
                }
            }
            long stop = System.currentTimeMillis();

            if (main.everythingDone)
                System.out.println("all done in " + (stop - start) + " ms.");
            else {
                System.out.println("could not do everything in "
                        + (stop - start) + " ms.");
                if (main.endedWithException != null)
                    main.endedWithException.printStackTrace();
            }
        } catch (InterruptedException e) {
            System.out.println("You've killed me!");
        }
    }
}

敬礼


3

这是一个非常简单易用的辅助类,可以运行或调用Java代码块:runcall

这基于BalusC的优秀answer

package com.mycompany.util.concurrent;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Calling {@link Callable#call()} or Running {@link Runnable#run()} code
 * with a timeout based on {@link Future#get(long, TimeUnit))}
 * @author pascaldalfarra
 *
 */
public class CallableHelper
{

    private CallableHelper()
    {
    }

    public static final void run(final Runnable runnable, int timeoutInSeconds)
    {
        run(runnable, null, timeoutInSeconds);
    }

    public static final void run(final Runnable runnable, Runnable timeoutCallback, int timeoutInSeconds)
    {
        call(new Callable<Void>()
        {
            @Override
            public Void call() throws Exception
            {
                runnable.run();
                return null;
            }
        }, timeoutCallback, timeoutInSeconds); 
    }

    public static final <T> T call(final Callable<T> callable, int timeoutInSeconds)
    {
        return call(callable, null, timeoutInSeconds); 
    }

    public static final <T> T call(final Callable<T> callable, Runnable timeoutCallback, int timeoutInSeconds)
    {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        try
        {
            Future<T> future = executor.submit(callable);
            T result = future.get(timeoutInSeconds, TimeUnit.SECONDS);
            System.out.println("CallableHelper - Finished!");
            return result;
        }
        catch (TimeoutException e)
        {
            System.out.println("CallableHelper - TimeoutException!");
            if(timeoutCallback != null)
            {
                timeoutCallback.run();
            }
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        catch (ExecutionException e)
        {
            e.printStackTrace();
        }
        finally
        {
            executor.shutdownNow();
            executor = null;
        }

        return null;
    }

}

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