我正在 Tomcat 上试验 Spring 的 DeferredResult
,我得到了疯狂的结果。是我做错了什么,还是 Spring 或 Tomcat 中存在一些错误?我的代码很简单。
@Controller
public class Test {
private DeferredResult<String> deferred;
static class DoSomethingUseful implements Runnable {
public void run() {
try { Thread.sleep(2000); } catch (InterruptedException e) { }
}
}
@RequestMapping(value="/test/start")
@ResponseBody
public synchronized DeferredResult<String> start() {
deferred = new DeferredResult<>(4000L, "timeout\n");
deferred.onTimeout(new DoSomethingUseful());
return deferred;
}
@RequestMapping(value="/test/stop")
@ResponseBody
public synchronized String stop() {
deferred.setResult("stopped\n");
return "ok\n";
}
}
所以。 start
请求创建一个超时为 4 秒的 DeferredResult
。 stop
请求将在 DeferredResult
上设置一个结果。如果您在延迟结果超时之前或之后发送 stop
,一切正常。
但是,如果您在 start
超时的同时发送 stop
,事情就会变得疯狂。我已经添加了一个 onTimeout
操作以使其易于重现,但这并不是问题发生所必需的。使用 APR 连接器,它只会死锁。使用 NIO 连接器时,它有时可以工作,但有时它会错误地将“超时”消息发送到 stop
客户端并且永远不会响应 start
客户端。
测试一下:
curl http://localhost/test/start & sleep 5; curl http://localhost/test/stop
我不认为我做错了什么。 Spring 文档似乎说可以随时调用 setResult
,即使在请求已经过期之后,也可以从任何线程(“the
应用程序可以从其选择的线程中生成结果”)。
使用的版本:Linux 上的 Tomcat 7.0.39,Spring 3.2.2。
最佳答案
这是一个很好的错误发现!
只需添加有关错误的更多信息 (that got fixed) 以便更好地理解。
setResult() 中有一个同步块(synchronized block),它扩展到提交分派(dispatch)的部分。如果同时发生超时,这可能会导致死锁,因为 Tomcat 超时线程有自己的锁定,只允许一个线程进行超时或分派(dispatch)处理。
详细解释:
当您在请求“超时”的同时调用“停止”时,两个线程正在尝试锁定 DeferredResult 对象“已延迟”。
执行“onTimeout”处理程序的线程 这是 Spring 文档的摘录:
This onTimeout method is called from a container thread when an async request times out before the DeferredResult has been set. It may invoke setResult or setErrorResult to resume processing.
执行“停止”服务的另一个线程。
如果在 stop() 服务期间调用的分派(dispatch)处理获得“延迟”锁,它将等待 tomcat 锁(比如 TomcatLock)完成分派(dispatch)。
如果另一个执行超时处理的线程已经获取了 TomcatLock,则该线程将等待获取 'deferred' 上的锁以完成 setResult()!
所以,我们最终陷入了典型的死锁局面!
关于java - Spring的DeferredResult setResult与超时的交互,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/16104980/