我有一个系统,当它收到来自网络服务的调用时,它会启动工作程序。 Worker 由 ExecutorService 启动,启动的类实现 Runnable。但是,如果工作线程超时,我无法真正杀死该工作线程,这会导致我的系统出现资源问题。
public class MyClass implements Runnable {
public void internalCall() {
logger.info("B-1");
//Some business code which may take too long
// <...>
logger.info("B-2");
}
public void launch() {
// Wrapper
Callable<Object> callable = new Callable<Object>() {
@Override
public Object call() throws Exception {
internalCall();
return null;
}
};
// Submit
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Object> future = executor.submit(callable);
try {
// Wait
future.get(1, TimeUnit.SECONDS);
}
catch (TimeoutException e) {
logger.warn("Timeout");
}
finally {
logger.info("A-1");
executor.shutdownNow();
future.cancel(true);
logger.info("A-2");
}
}
}
如果工作线程超时,我预计会出现以下日志消息:
INFO | B-1
WARN | Timeout
INFO | A-1
INFO | A-2
随后服务保持空闲状态,直到另一个工作程序请求到来。但是,尽管分别在 ExecutorService 和 Future 上调用 shutdownNow() 和 cancel(),工作程序仍继续:
INFO | B-1
WARN | Timeout
INFO | A-1
INFO | A-2
INFO | B-2
我环顾四周,发现还有许多其他关于终止线程的类似问题,普遍的共识是你不应该这样做。然而,这是一个可以扩展的类,目的是覆盖internalCall() - 这意味着我不能依赖internalCall来监管自身并检查Thread.isInterrupted()或类似的东西。
我想通过攻击furure或executor对象来强制杀死launch()方法中的东西。
注意
如果线程没有响应Thread.interrupt怎么办?
在某些情况下,您可以使用特定于应用程序的技巧。例如, 如果线程正在等待已知的套接字,您可以关闭该套接字 导致线程立即返回。不幸的是,确实有 不是任何通用的技术。 需要注意的是,在 等待线程不响应的所有情况 Thread.interrupt,它也不会响应Thread.stop。 这样 案例包括故意拒绝服务攻击和 I/O 操作 thread.stop 和 thread.interrupt 无法正常工作。 - Java 线程原语弃用
那么我们学到了什么......在 Java 中,不要运行您确实无法信任的第三方代码,其执行方式与您的主应用程序相同。另外,即使 Thread.stop 确实起作用,你仍然有一大堆其他事情比不检查中断状态的线程要糟糕得多(即代码调用
System.exit(0)
)。
我建议您对不能信任的第三方代码执行以下操作之一:
将第三方代码作为由您控制执行的 Java 运行的评估语言来运行。一些例子是:像 Drools 这样的规则语言或像 JMustache 这样的无逻辑模板语言。
在单独的执行中运行第三方代码,并使用您的操作系统杀死进程和IPC(例如用于通信的套接字)。
首先,
future.cancel(true)
不会杀死正在运行的线程。它只是试图以“礼貌的方式”停止其执行。
它的作用是发送中断信号,或多或少,就像你在代码中的某个地方调用
yourthread.interrupt()
一样。
仅当 run() 方法内的代码检查中断时,它才会停止处理。因此,在您的
internalCall()
内部,您需要时不时地检查线程是否没有被调用 Thread.interrupted()
中断并停止执行。中断还会通过抛出 InterruptedExcepiton 来停止 sleep()、wait()、IO 操作(这就是这些方法抛出此类异常的原因)。
ExecutorService
使用相同的机制,因此它将尝试中断正在运行的线程,但同样,如果您的代码不检查此类中断,则线程将继续运行。
由于各种原因,线程不应该被简单地杀死(检查java文档中的thread.stop()方法以及为什么它被弃用),通知线程可能应该停止工作的“通用”方法是中断信号。
我使用 SingleThreadExecutor 和 Future 进行了相同的设置。但由于我无法控制我执行的代码,因此我无法实现对
Thread.interrupted()
的检查。我的解决方案是使用普通的
Thread
对象和 TimerTask
。
var executionThread = new ExecutionThread(/* params... */);
var timer = new Timer();
var timeOutTask = new TimeOutTask(executionThread, timer);
timer.schedule(timeOutTask, MAX_EXECUTION_TIME_SECONDS * 1_000);
executionThread.start();
try {
// wait for the execution to finish:
executionThread.join();
} catch (InterruptedException e) {
// I throw an exception here for example, but this case will usually not happen
}
if (timeOutTask.isThreadTimedOut()) {
// custom exception I publish to the client, you can handle it different though
throw new ExecutionTimedOut(MAX_TEST_EXECUTION_TIME_SECONDS);
}
// the result that you would usually receive from the future.
return executionThread.getResult();
自定义线程类可以如下所示:
(我用的是lombok,你也可以手动写出构造函数和getter)
@RequiredArgsConstructor
class ExecutionThread extends Thread {
@Getter private Result result;
private final Things i_need_for_execution;
@Override
public void run() {
result = // execute the task here ...
}
}
计时器可以这样复制:
@RequiredArgsConstructor
class TimeOutTask extends TimerTask {
private final Thread thread;
private final Timer timer;
@Getter private boolean threadTimedOut = false;
@Override
public void run() {
if (thread != null && thread.isAlive()) {
//noinspection deprecation
thread.stop();
timer.cancel();
threadTimedOut = true;
}
}
}
我希望有一个不涉及已弃用的
Thread::stop
的解决方案,但这是我找到的唯一方法。