【问题标题】:Filter out duplicates for CompletableFuture过滤掉 CompletableFuture 的重复项
【发布时间】:2018-11-01 00:11:08
【问题描述】:

我想过滤掉第一个CompletableFuture 之后的重复项,然后使用另一个CompletableFuture 调用第二个阶段。我尝试了什么:

@FunctionalInterface
public interface FunctionWithExceptions<T, R, E extends Exception> {
    R process(T t) throws E;
}


public static <T> Predicate<T> distinctByKey(FunctionWithExceptions<? super T, ?, ?> keyExtractor) {
    Set<Object> seen = ConcurrentHashMap.newKeySet();
    return t -> {
        String key = "";
        try {
            key = (String) keyExtractor.process(t);
        } catch (Exception e) {
            log.info("Get instanceIp failed!");
        }
        return seen.add(key);
    };
}

List<CompletableFuture<InstanceDo>> instanceFutures = podNames.stream()
            .map(podName -> CompletableFuture.supplyAsync(RethrowExceptionUtil.rethrowSupplier(() -> {
                PodDo podDo = getPodRetriever().getPod(envId, podName);
                podDoList.add(podDo);
                return podDo;
            }), executor))
            .map(future -> future.thenApply(podDo -> podDo.getInstanceName()))
            .filter(distinctByKey(CompletableFuture::get))
            .map(future -> future.thenCompose(instanceName ->
                    CompletableFuture.supplyAsync(() -> get(envId, instanceName), executor)))
            .collect(Collectors.toList());

如您所见,distinctByKey 将调用 get,这将直接使并发变为顺序性。

我应该怎么做才能使它再次CONCURRENT但同时保留distinct功能?

或

我只有一个选择?

要等待整个第一阶段完成,然后开始第二阶段?

【问题讨论】:

  • 小心,thenApply 和 thenCompose 是同步的。您可能更喜欢thenApplyAsync 和thenComposeAsync。
  • @kagmole 感谢您的回复。但我确实不认为你明白这一点。我对 *Async 非常了解,因为它们有据可查。我希望 second 在 first 和 thenCompose 之后运行就足够了,因为它将与 first 在同一个线程中工作 阶段。
  • 我明白了,确实还有distinctByKey 的问题。我可能有一个基于Optional 的建议,但我担心它最终会变得不必要地臃肿。
  • @kagmole 感谢您的idea,我会试一试。 B.T.W 实际上从一开始就是 distinct 问题。哈哈,谢谢你,伙计。
  • 是的,抱歉我不是很清楚。 :)

标签: java java-stream distinct completable-future


【解决方案1】:

我只是写了一个简单的演示来解决这种问题,但我真的不知道它是否可靠。但至少它确保可以使用Set&lt;Object&gt; seen = ConcurrentHashMap.newKeySet(); 加速第二阶段。

public static void main(String... args) throws ExecutionException, InterruptedException {
        Set<Object> seen = ConcurrentHashMap.newKeySet();
        List<CompletableFuture<Integer>> intFutures = Stream.iterate(0, i -> i+1)
                .limit(5)
                .map(i -> CompletableFuture.supplyAsync(() -> {
                    int a = runStage1(i);
                    if (seen.add(a)) {
                        return a;
                    } else {
                        return -1;
                    }}))
                .map(future -> future.thenCompose(i -> CompletableFuture.supplyAsync(() -> {
                    if (i > 0) {
                        return runStage2(i);
                    } else {
                        return i;
                    }})))
                .collect(Collectors.toList());
        List<Integer> resultList = new ArrayList<>();
        try {
            for (CompletableFuture<Integer> future: intFutures) {
                resultList.add(future.join());
            }
        } catch (Exception ignored) {
            ignored.printStackTrace();
            out.println("Future failed!");
        }
        resultList.stream().forEach(out::println);
    }

    private static Integer runStage1(int a) {
        out.println("stage - 1: " + a);
        try {
            Thread.sleep(500 + Math.abs(new Random().nextInt()) % 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return Integer.valueOf(a % 3);
    }

    private static Integer runStage2(int b) {
        out.println("stage - 2: " + b);
        try {
            Thread.sleep(200 + Math.abs(new Random().nextInt()) % 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return Integer.valueOf(b);
    }

通过在复制时在第一阶段返回特殊值,然后在第二阶段返回,通过特殊值(-1),我可以忽略耗时的第二阶段计算。

输出确实过滤掉了第二阶段的一些冗余计算。

stage - 1: 0
stage - 1: 1
stage - 1: 2
stage - 1: 3
stage - 2: 2 // 
stage - 2: 1 //
stage - 1: 4
0
1
2
-1
-1

我认为这不是一个好的解决方案。但是我可以优化什么来让它变得更好呢?

【讨论】:

  • 为什么你认为这不是一个好的解决方案?我看到的唯一问题是结果列表包含第一阶段(-1)和第二阶段的结果的混合,但您可以轻松地将它们过滤掉。
  • 您应该使用ThreadLocalRandom.current() 而不是new Random(),因为它是线程安全的并且是首选替代方案。您的解决方案与我的想法相似,您只是使用了鉴别器值-1 而不是Optional,如果你能做到这一点,那就太好了(没有Optional 开销)。不过,我现在看不到任何改进。
  • @kagmole 感谢您的帮助,我明白了这一点并以这种方式对其进行了改进。此外,感谢使用 thread-safe random 的软提醒,我忘记了。谢谢~
  • @DidierL 谢谢你的观点,我刚刚测试了它们,看起来不错。我认为它可以更优雅 - 直接过滤而不是在第二阶段检查它然后过滤。
【解决方案2】:

与your submitted answer 相比,一个小的改进可能是使用ConcurrentHashMap 作为一种缓存,这样您的最终列表就会包含相同的结果,而与您获得它们的顺序无关:

Map<Integer, CompletableFuture<Integer>> seen = new ConcurrentHashMap<>();
List<CompletableFuture<Integer>> intFutures = Stream.iterate(0, i -> i + 1)
        .limit(5)
        .map(i -> CompletableFuture.supplyAsync(() -> runStage1(i)))
        .map(cf -> cf.thenCompose(result ->
                seen.computeIfAbsent(
                        result, res -> CompletableFuture.supplyAsync(() -> runStage2(res))
                )
        ))
        .collect(Collectors.toList());

请注意,传递给computeIfAbsent() 的函数立即返回很重要(例如使用supplyAsync()),因为它在执行时会在映射内保持锁定。此外,此函数不得尝试修改seen 映射,因为it could cause issues。

通过此更改,输出可能是例如:

stage - 1: 1
stage - 1: 0
stage - 1: 2
stage - 2: 1
stage - 2: 2
stage - 1: 3
stage - 2: 0
stage - 1: 4
0
1
2
0
1

此外,这允许在所有期货完成后检查seen 映射以获得独特的结果。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-09-29
    • 1970-01-01
    • 2019-10-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-21
    • 1970-01-01
    相关资源
    最近更新 更多