[英]How to prevent CompletableFuture#whenComplete execution in context thread
I have the following code: 我有以下代码:
ConcurrentHashMap taskMap= new ConcurrentHashMap();
....
taskMap.compute(key, (k, queue) -> {
CompletableFuture<Void> future = (queue == null)
? CompletableFuture.runAsync(myTask, poolExecutor)
: queue.whenCompleteAsync((r, e) -> myTask.run(), poolExecutor);
//to prevent OutOfMemoryError in case if we will have too much keys
future.whenComplete((r, e) -> taskMap.remove(key, future));
return future;
});
The issue of this code that in case of future
already completed whenComplete
function argument invokes in the same thread as compute
invokes. 此代码的问题是
future
在whenComplete
函数参数与compute
调用相同的线程中调用时已经完成的情况。 In the body of this method we remove entry from map. 在此方法的主体中,我们从地图中删除条目。 But compute method documentation forbid this and application freezes.
但是计算方法文档禁止这样做,并且应用程序冻结。
How can I fix this issue? 如何解决此问题?
The most obvious solution is to use whenCompleteAsync
instead of whenComplete
, as the former guarantees to execute the action using the supplied Executor
rather than the calling thread. 最明显的解决方案是使用
whenCompleteAsync
而不是whenComplete
,因为前者保证使用提供的Executor
而不是调用线程来执行操作。 Which can be demonstrated with 可以证明
Executor ex = r -> { System.out.println("job scheduled"); new Thread(r).start(); };
for(int run = 0; run<2; run++) {
boolean completed = run==0;
System.out.println("*** "+(completed? "with already completed": "with async"));
CompletableFuture<String> source = completed?
CompletableFuture.completedFuture("created in "+Thread.currentThread()):
CompletableFuture.supplyAsync(() -> {
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
return "created in "+Thread.currentThread();
}, ex);
source.thenApplyAsync(s -> s+"\nprocessed in "+Thread.currentThread(), ex)
.whenCompleteAsync((s,t) -> {
if(t!=null) t.printStackTrace(); else System.out.println(s);
System.out.println("consumed in "+Thread.currentThread());
}, ex)
.join();
}
which will print something like 这将打印类似
*** with already completed
job scheduled
job scheduled
created in Thread[main,5,main]
processed in Thread[Thread-0,5,main]
consumed in Thread[Thread-1,5,main]
*** with async
job scheduled
job scheduled
job scheduled
created in Thread[Thread-2,5,main]
processed in Thread[Thread-3,5,main]
consumed in Thread[Thread-4,5,main]
So you could just use 所以你可以用
taskMap.compute(key, (k, queue) -> {
CompletableFuture<Void> future = (queue == null)
? CompletableFuture.runAsync(myTask, poolExecutor)
: queue.whenCompleteAsync((r, e) -> myTask.run(), poolExecutor);
//to prevent OutOfMemoryError in case if we will have too much keys
future.whenCompleteAsync((r, e) -> taskMap.remove(key, future), poolExecutor);
return future;
});
If early completion has a significant likelihood, you could reduce the overhead using 如果提前完成的可能性很大,则可以使用以下方法减少开销:
taskMap.compute(key, (k, queue) -> {
CompletableFuture<Void> future = (queue == null)
? CompletableFuture.runAsync(myTask, poolExecutor)
: queue.whenCompleteAsync((r, e) -> myTask.run(), poolExecutor);
//to prevent OutOfMemoryError in case if we will have too much keys
if(future.isDone()) future = null;
else future.whenCompleteAsync((r, e) -> taskMap.remove(key, future), poolExecutor);
return future;
});
Maybe, you didn't come to this obvious solution, because you don't like that the dependent action will always be scheduled as a new task to the pool, even if the completion happens in a different task already. 也许您没有想到这个显而易见的解决方案,因为您不希望将依赖操作始终作为新任务调度到池中,即使完成已经发生在另一个任务中也是如此。 You could solve this with a specialized executor which will only reschedule the task when necessary:
您可以使用专门的执行程序解决此问题,该执行程序仅在必要时重新计划任务:
Executor inPlace = Runnable::run;
Thread forbidden = Thread.currentThread();
Executor forceBackground
= r -> (Thread.currentThread()==forbidden? poolExecutor: inPlace).execute(r);
…
future.whenCompleteAsync((r, e) -> taskMap.remove(key, future), forceBackground);
But you may rethink whether this complicated per-mapping cleanup logic is really desired. 但是您可能会重新考虑是否真的需要这种复杂的按映射清理逻辑。 It's not only complicated but also may create a notable overhead, potentially scheduling lots of cleanup actions that are not really required when they are already outdated when executed.
这不仅很复杂,而且可能会产生显着的开销,可能会安排许多清理操作,而这些清理操作在执行时已经过时了,它们并不是真正需要的。
It might be much simpler and even more efficient to execute 执行起来可能会更简单甚至更有效率
taskMap.values().removeIf(CompletableFuture::isDone);
from time to time to cleanup the entire map. 不时清理整个地图。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.