简体   繁体   English

如何防止在上下文线程中执行CompletableFuture#whenComplete

[英]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. 此代码的问题是futurewhenComplete函数参数与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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM