我正在编写一个异步客户端。每次发送请求时,客户端都会在内部创建,注册,然后返回CompletableFuture
,一旦收到服务器对发送请求的响应,该请求便会完成。
问题是,某些请求永远不会收到响应,并且客户端代码会在超时后放弃这些请求。因此,我最终获得了多个已注册的CompletableFuture
,客户端代码不再保留对其的引用,因此应该可以进行垃圾回收。
为了避免内存泄漏,我决定更改客户端的实现,以将WeakReference
保留为返回的CompletableFuture
而不是强数,以使未引用的期货获得如果客户端代码不再需要它们,则将其驱逐。
以下(简化的)代码演示了客户端:
interface Message {
Object getID();
byte[] toByteArray();
}
abstract class AsyncClient<T extends Message, R extends Message> {
/**
* This map serves as a registry of CompletableFutures for sent requests.
*
* Previously it was Map<Object, CompletableFuture<R>>. Changed to References
* to avoid CompletableFutures staying in registry after the recipient code
* does not wait for them anymore.
*/
Map<Object, Reference<CompletableFuture<R>>> requestFutureRegistry = new ConcurrentHashMap<>();
/**
* Write the request to the service
* @param serializedRequest bytes to write
*/
protected abstract void writeRequest(byte[] serializedRequest);
/**
* Make sure to clean up the registry from Futures that arte not held by the
* recipient code anymore.
*/
private void deleteDanglingFutures() {
requestFutureRegistry.entrySet().removeIf(entry -> entry.getValue().get() == null);
}
/**
*
* @param request to be written to service
* @return a CompletableFuture for the request's response
*/
public CompletableFuture<R> sendRequest(T request) {
deleteDanglingFutures();
CompletableFuture<R> future = new CompletableFuture<>();
requestFutureRegistry.put(request.getID(), new WeakReference<>(future));
writeRequest(request.toByteArray());
return future;
}
void receivedResponse(R response) {
deleteDanglingFutures();
Reference<CompletableFuture<R>> futureRef = requestFutureRegistry.get(response.getID());
if (futureRef != null) {
CompletableFuture<R> future = futureRef.get();
if (future != null) {
future.complete(response);
}
}
}
}
这种方法似乎有效,直到我开始使用thenXXX方法将CompletableFuture
链接起来。例如,当我执行以下操作时:
AsyncClient client;
Request request1;
// ...
Response response = client.sendRequest(request1)
.thenCompose(response1 ->
client.sendRequest(request2)
).get(timeout, TimeUnit.MILLISECONDS);
我注意到内部的未来(从对client.sendRequest(request2)
的呼叫)有时在获得完成机会之前就已经进行了GC。
深入研究JDK实现,似乎没有对内部未来的强大参考存储(如this post中所述)。
我的问题是-如果客户代码不再(直接或通过thenXXX链接)引用已注册的期货,并且仍保留足够长的时间以防万一,则正确的方法是确保注销已注册的期货还需要吗?
编辑+动机
我现在了解(感谢@Holger)Future::get
不是CompletableFuture
的常用用法,但是,在我的情况下,这很常见。具体来说,我需要支持的一个用例是发送多个请求,等待第一个请求响应(使用anyOf
,然后使用get
超时)。事先知道,许多请求将永远不会从服务中得到响应,因此我需要(最好是自动地)在超时后清理未使用的期货。