为什么异常完成前调用get()会等待异常执行?
Why calling get() before exceptional completion waits for exceptionally to execute?
在回答 this question 时,我注意到 CompletableFuture
的一个奇怪行为:如果你有一个 CompletableFuture cf
并与 cf.exceptionally()
链接一个电话,调用 cf.get()
表现异常:
- 如果您在异常完成之前调用它,它会在返回前等待
exceptionally()
块的执行
- 否则,它会立即失败,抛出预期的
ExecutionException
我是不是遗漏了什么或者这是一个错误?我在 Ubuntu 17.04.
上使用 Oracle JDK 1.8.0_131
下面的代码说明了这种现象:
public static void main(String[] args) {
long start = System.currentTimeMillis();
final CompletableFuture<Object> future = CompletableFuture.supplyAsync(() -> {
sleep(1000);
throw new RuntimeException("First");
}).thenApply(Function.identity());
future.exceptionally(e -> {
sleep(1000);
logDuration(start, "Exceptionally");
return null;
});
final CompletableFuture<Void> futureA = CompletableFuture.runAsync(() -> {
try {
future.get();
} catch (Exception e) {
} finally {
logDuration(start, "A");
}
});
final CompletableFuture<Void> futureB = CompletableFuture.runAsync(() -> {
sleep(1100);
try {
future.get();
} catch (Exception e) {
} finally {
logDuration(start, "B");
}
});
try {
future.join();
} catch (Exception e) {
logDuration(start, "Main");
}
futureA.join();
futureB.join();
}
private static void sleep(final int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static void logDuration(long start, String who) {
System.out.println(who + " waited for " + (System.currentTimeMillis() - start) + "ms");
}
输出:
B waited for 1347ms
Exceptionally waited for 2230ms
Main waited for 2230ms
A waited for 2230ms
如您所见,futureB
在调用 get()
之前休眠了一会儿,根本没有阻塞。但是,futureA
和主线程都在等待 exceptionally()
完成。
请注意,如果删除 .thenApply(Function.identity())
.
,则不会发生此行为
唤醒休眠线程是一个依赖操作,必须像其他任何操作一样处理,并且没有优先级。另一方面,一个线程轮询一个 CompletableFuture
当它已经完成时不会进入休眠状态,不需要被唤醒,因此不需要与其他依赖动作竞争。
用下面的程序
public static void main(String[] args) {
final CompletableFuture<Object> future = CompletableFuture.supplyAsync(() -> {
waitAndLog("Supplier", null, 1000);
throw new RuntimeException("First");
}).thenApply(Function.identity());
long start = System.nanoTime();
CompletableFuture.runAsync(() -> waitAndLog("A", future, 0));
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10));
future.exceptionally(e -> {
waitAndLog("Exceptionally", null, 1000);
return null;
});
CompletableFuture.runAsync(() -> waitAndLog("B", future, 0));
CompletableFuture.runAsync(() -> waitAndLog("C", future, 1100));
waitAndLog("Main", future, 0);
ForkJoinPool.commonPool().awaitQuiescence(10, TimeUnit.SECONDS);
}
private static void waitAndLog(String msg, CompletableFuture<?> primary, int sleep) {
long nanoTime = System.nanoTime();
Object result;
try {
if(sleep>0) Thread.sleep(sleep);
result = primary!=null? primary.get(): null;
} catch (InterruptedException|ExecutionException ex) {
result = ex;
}
long millis=TimeUnit.NANOSECONDS.toMillis(System.nanoTime()-nanoTime);
System.out.println(msg+" waited for "+millis+"ms"+(result!=null? ", got "+result: ""));
}
我明白了,
Supplier waited for 993ms
A waited for 993ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First
C waited for 1108ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First
Exceptionally waited for 998ms
Main waited for 1983ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First
B waited for 1984ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First
在我的机器上,这表明 在这个特定的 案例中,相关操作 是按照计划的顺序执行的,A
第一。请注意,我在安排 Exceptionally
之前插入了额外的等待时间,这将是下一个相关操作。由于 B
在后台线程中运行,因此它是否设法在 Main
线程之前安排自己是不确定的。我们可以在执行命令之前插入另一个延迟。
由于C
轮询一个已经完成的未来,它可以立即进行,所以它的净等待时间接近于明确指定的休眠时间。
必须强调的是,这只是特定场景的结果,具体取决于实施细节。没有保证依赖操作的执行顺序。您自己可能已经注意到,如果没有 .thenApply(Function.identity())
步骤,实现会运行不同的代码路径,从而导致相关操作的执行顺序不同。
依赖关系形成一棵树,实现必须以有效的方式遍历它而不会有堆栈溢出的风险,因此它必须以某种方式将其展平,并且依赖关系树形状的微小变化可能会影响结果以非直观的方式排序。
在回答 this question 时,我注意到 CompletableFuture
的一个奇怪行为:如果你有一个 CompletableFuture cf
并与 cf.exceptionally()
链接一个电话,调用 cf.get()
表现异常:
- 如果您在异常完成之前调用它,它会在返回前等待
exceptionally()
块的执行 - 否则,它会立即失败,抛出预期的
ExecutionException
我是不是遗漏了什么或者这是一个错误?我在 Ubuntu 17.04.
上使用 Oracle JDK 1.8.0_131下面的代码说明了这种现象:
public static void main(String[] args) {
long start = System.currentTimeMillis();
final CompletableFuture<Object> future = CompletableFuture.supplyAsync(() -> {
sleep(1000);
throw new RuntimeException("First");
}).thenApply(Function.identity());
future.exceptionally(e -> {
sleep(1000);
logDuration(start, "Exceptionally");
return null;
});
final CompletableFuture<Void> futureA = CompletableFuture.runAsync(() -> {
try {
future.get();
} catch (Exception e) {
} finally {
logDuration(start, "A");
}
});
final CompletableFuture<Void> futureB = CompletableFuture.runAsync(() -> {
sleep(1100);
try {
future.get();
} catch (Exception e) {
} finally {
logDuration(start, "B");
}
});
try {
future.join();
} catch (Exception e) {
logDuration(start, "Main");
}
futureA.join();
futureB.join();
}
private static void sleep(final int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static void logDuration(long start, String who) {
System.out.println(who + " waited for " + (System.currentTimeMillis() - start) + "ms");
}
输出:
B waited for 1347ms
Exceptionally waited for 2230ms
Main waited for 2230ms
A waited for 2230ms
如您所见,futureB
在调用 get()
之前休眠了一会儿,根本没有阻塞。但是,futureA
和主线程都在等待 exceptionally()
完成。
请注意,如果删除 .thenApply(Function.identity())
.
唤醒休眠线程是一个依赖操作,必须像其他任何操作一样处理,并且没有优先级。另一方面,一个线程轮询一个 CompletableFuture
当它已经完成时不会进入休眠状态,不需要被唤醒,因此不需要与其他依赖动作竞争。
用下面的程序
public static void main(String[] args) {
final CompletableFuture<Object> future = CompletableFuture.supplyAsync(() -> {
waitAndLog("Supplier", null, 1000);
throw new RuntimeException("First");
}).thenApply(Function.identity());
long start = System.nanoTime();
CompletableFuture.runAsync(() -> waitAndLog("A", future, 0));
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10));
future.exceptionally(e -> {
waitAndLog("Exceptionally", null, 1000);
return null;
});
CompletableFuture.runAsync(() -> waitAndLog("B", future, 0));
CompletableFuture.runAsync(() -> waitAndLog("C", future, 1100));
waitAndLog("Main", future, 0);
ForkJoinPool.commonPool().awaitQuiescence(10, TimeUnit.SECONDS);
}
private static void waitAndLog(String msg, CompletableFuture<?> primary, int sleep) {
long nanoTime = System.nanoTime();
Object result;
try {
if(sleep>0) Thread.sleep(sleep);
result = primary!=null? primary.get(): null;
} catch (InterruptedException|ExecutionException ex) {
result = ex;
}
long millis=TimeUnit.NANOSECONDS.toMillis(System.nanoTime()-nanoTime);
System.out.println(msg+" waited for "+millis+"ms"+(result!=null? ", got "+result: ""));
}
我明白了,
Supplier waited for 993ms
A waited for 993ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First
C waited for 1108ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First
Exceptionally waited for 998ms
Main waited for 1983ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First
B waited for 1984ms, got java.util.concurrent.ExecutionException: java.lang.RuntimeException: First
在我的机器上,这表明 在这个特定的 案例中,相关操作 是按照计划的顺序执行的,A
第一。请注意,我在安排 Exceptionally
之前插入了额外的等待时间,这将是下一个相关操作。由于 B
在后台线程中运行,因此它是否设法在 Main
线程之前安排自己是不确定的。我们可以在执行命令之前插入另一个延迟。
由于C
轮询一个已经完成的未来,它可以立即进行,所以它的净等待时间接近于明确指定的休眠时间。
必须强调的是,这只是特定场景的结果,具体取决于实施细节。没有保证依赖操作的执行顺序。您自己可能已经注意到,如果没有 .thenApply(Function.identity())
步骤,实现会运行不同的代码路径,从而导致相关操作的执行顺序不同。
依赖关系形成一棵树,实现必须以有效的方式遍历它而不会有堆栈溢出的风险,因此它必须以某种方式将其展平,并且依赖关系树形状的微小变化可能会影响结果以非直观的方式排序。