1. 概述

在本文中,我们将探讨如何在 Java 程序中控制长时间运行的任务,在超过指定时间后自动停止执行。我们会介绍几种不同的实现方式,并分析它们各自的优缺点和潜在问题。

2. 使用循环控制时间

假设我们正在处理一组数据项,比如电商系统中的商品信息,但我们并不需要处理完所有数据。我们希望最多处理 30 秒钟的数据,然后停止执行并返回已处理的部分结果。

一个简单的实现方式如下:

long start = System.currentTimeMillis();
long end = start + 30 * 1000;
while (System.currentTimeMillis() < end) {
    // 执行耗时操作
}

这个循环会在 30 秒后退出。但这种做法存在以下问题:

精度差:循环可能运行超过设定的时间,具体取决于每次迭代的耗时。比如每次迭代耗时 7 秒,那么总耗时可能达到 35 秒,超出设定时间约 17%。

阻塞主线程:如果在主线程中执行这样的循环,会阻塞主线程较长时间,影响程序响应性。

为了解决这些问题,我们可以使用中断机制,将任务交给子线程执行。

3. 使用中断机制控制任务执行

我们将任务放在单独的线程中执行,主线程在超时后发送中断信号,让子线程主动退出。

3.1. 线程中断的基本写法

class LongRunningTask implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < Long.MAX_VALUE; i++) {
            if(Thread.interrupted()) {
                return;
            }
        }
    }
}

这段代码模拟一个长时间运行的任务。注意:不是所有操作都响应中断,所以我们需要手动检查中断标志位,并在每次循环中都检查,确保能及时响应中断。

3.2. 使用 Timer 实现超时中断

我们可以通过 TimerTimerTask 来安排中断任务:

class TimeOutTask extends TimerTask {
    private Thread thread;
    private Timer timer;

    public TimeOutTask(Thread thread, Timer timer) {
        this.thread = thread;
        this.timer = timer;
    }

    @Override
    public void run() {
        if(thread != null && thread.isAlive()) {
            thread.interrupt();
            timer.cancel();
        }
    }
}

使用方式如下:

Thread thread = new Thread(new LongRunningTask());
thread.start();

Timer timer = new Timer();
TimeOutTask timeOutTask = new TimeOutTask(thread, timer);
timer.schedule(timeOutTask, 3000);

这种方式简单直接,但使用的是单线程的 Timer,无法复用线程池。

3.3. 使用 Future#get 实现超时中断

ExecutorService executor = Executors.newSingleThreadExecutor();
Future future = executor.submit(new LongRunningTask());
try {
    future.get(7, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    future.cancel(true);
} catch (Exception e) {
    // 处理其他异常
} finally {
    executor.shutdownNow();
}

这种方式通过 Future.get(timeout, unit) 阻塞等待任务完成,超时后抛出 TimeoutException,此时我们调用 future.cancel(true) 发送中断信号。

优点是使用了线程池,性能更好。

3.4. 使用 ScheduledExecutorService 实现中断

这是推荐的方式:

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
Future future = executor.submit(new LongRunningTask());
Runnable cancelTask = () -> future.cancel(true);

executor.schedule(cancelTask, 3000, TimeUnit.MILLISECONDS);
executor.shutdown();

我们使用 ScheduledExecutorService 来调度一个取消任务,3 秒后自动调用 future.cancel(true)

✅ 优点:不阻塞主线程,线程池管理更高效,是目前最推荐的做法。

4. 是否能保证准时停止?

⚠️ 不能保证任务一定在指定时间后立即停止

原因在于:不是所有阻塞操作都支持中断。Java 中只有少数方法是可中断的,比如 Object.wait()Thread.sleep() 等。

例如:

  • BufferedReader.read() 是不可中断的,即使调用了 interrupt() 也不会立即返回。
  • InterruptibleChannelread() 方法是可中断的。

建议做法:在循环中手动检查中断标志位,虽然不能严格保证时间,但可以尽早退出。

⚠️ 禁止使用 Thread.stop():这个方法已经被标记为废弃,因为它会导致线程持有的锁被强制释放,可能造成数据不一致。

5. 设计可中断的任务

为了更好地控制任务执行时间,我们应该从设计上就支持中断。

5.1. 定义可中断的步骤

class Step {
    private static int MAX = Integer.MAX_VALUE/2;
    int number;

    public Step(int number) {
        this.number = number;
    }

    public void perform() throws InterruptedException {
        Random rnd = new Random();
        int target = rnd.nextInt(MAX);
        while (rnd.nextInt(MAX) != target) {
            if (Thread.interrupted()) {
                throw new InterruptedException();
            }
        }
    }
}

每一步都检查中断标志,如果被中断则抛出异常。

5.2. 定义任务执行器

public class SteppedTask implements Runnable {
    private List<Step> steps;

    public SteppedTask(List<Step> steps) {
        this.steps = steps;
    }

    @Override
    public void run() {
        for (Step step : steps) {
            try {
                step.perform();
            } catch (InterruptedException e) {
                return;
            }
        }
    }
}

5.3. 使用示例

List<Step> steps = Stream.of(
  new Step(1),
  new Step(2),
  new Step(3),
  new Step(4))
.collect(Collectors.toList());

Thread thread = new Thread(new SteppedTask(steps));
thread.start();

Timer timer = new Timer();
TimeOutTask timeOutTask = new TimeOutTask(thread, timer);
timer.schedule(timeOutTask, 10000);

该设计确保了任务可以在任意步骤中被中断,虽然不能精确控制时间,但比完全不可中断的任务要好得多。

6. 总结

在这篇文章中,我们探讨了几种控制 Java 任务执行时间的方法:

方法 优点 缺点
循环检查时间 简单直接 精度差、阻塞主线程
Timer + interrupt 实现简单 单线程、功能有限
Future.get 支持线程池 阻塞主线程
ScheduledExecutorService 高效、非阻塞 最佳实践

虽然不能完全保证任务在指定时间精确停止,但通过合理使用中断机制,可以大大提升程序的可控性和健壮性。

完整代码可以在 GitHub 上找到。


原始标题:How to Stop Execution After a Certain Time in Java