Java 8이 출시되었습니다. 이제 새로운 것을 배울 시간입니다. Java 7과 Java 6은 약간 수정된 버전이지만 Java 8은 크게 개선되었습니다. 아마도 Java 8이 너무 클까요? 오늘은 JDK 8의 새로운 추상화 CompletableFuture에 대해 자세히 설명하겠습니다. 우리 모두 알고 있듯이 Java 8은 1년 이내에 출시될 예정이므로 이 기사는 람다를 지원하는 JDK 8 빌드 88을 기반으로 합니다. CompletableFuture는 Future를 확장하여 메소드, 단항 연산자를 제공하고 이전 버전의 Java에서 멈추지 않는 비동기성과 이벤트 중심 프로그래밍 모델을 촉진합니다. CompletableFuture의 JavaDoc을 열면 충격을 받을 것입니다. 약 50가지 방법(!)이 있으며 그 중 일부는 매우 흥미롭고 이해하기 어렵습니다. 예를 들면 다음과 같습니다.
다음과 같이 코드를 복사합니다. public <U,V> CompletableFuture<V> thenCombineAsync(
CompletableFuture<? 확장 U> 기타,
BiFunction<? 슈퍼 T,? 슈퍼 U,? 확장 V> fn,
집행자 집행자)
걱정하지 말고 계속 읽어보세요. CompletableFuture는 Guava 및 SettableFuture에서 ListenableFuture의 모든 특성을 수집합니다. 또한 내장된 람다 표현식을 통해 Scala/Akka 퓨처에 더 가까워졌습니다. 이것은 사실이라고 믿기에는 너무 좋게 들릴 수도 있지만 계속 읽어보세요. CompletableFuture에는 ol의 Future 비동기 콜백/변환보다 뛰어난 두 가지 주요 측면이 있습니다. 이를 통해 CompletableFuture의 값을 언제든지 모든 스레드에서 설정할 수 있습니다.
1. 패키지 값 추출 및 수정
future는 다른 스레드에서 실행되는 코드를 나타내는 경우가 많지만 항상 그런 것은 아닙니다. 때로는 JMS 메시지 도착과 같이 무슨 일이 일어날지 알고 있음을 나타내기 위해 Future를 생성하고 싶을 때도 있습니다. 따라서 미래는 있지만 미래에는 잠재적인 비동기 작업이 없습니다. 이벤트에 의해 구동되는 향후 JMS 메시지가 도착하면 간단히 완료(해결)되기를 원할 뿐입니다. 이 경우 간단히 CompletableFuture를 생성하여 클라이언트에 반환할 수 있으며, 결과가 사용 가능하다고 생각하는 한 간단히 Complete()를 통해 Future를 기다리는 모든 클라이언트의 잠금을 해제할 수 있습니다.
먼저 새로운 CompletableFuture를 생성하여 클라이언트에 제공할 수 있습니다.
다음과 같이 코드를 복사합니다. public CompletableFuture<String> Ask() {
final CompletableFuture<String> future = new CompletableFuture<>();
//...
미래를 반환;
}
이 future는 Callable과 연결되지 않고 스레드 풀도 없으며 비동기적으로 작동하지 않습니다. 클라이언트 코드가 이제 Ask().get()을 호출하면 영원히 차단됩니다. 레지스터가 콜백을 완료하면 결코 적용되지 않습니다. 그렇다면 핵심은 무엇입니까? 이제 다음과 같이 말할 수 있습니다.
다음과 같이 코드를 복사하세요: future.complete("42")
...이 시점에서 모든 클라이언트 Future.get()은 문자열의 결과를 가져오고 콜백 완료 후 즉시 적용됩니다. 이는 Future의 작업을 표현하고 일부 실행 스레드의 작업을 계산할 필요가 없을 때 매우 편리합니다. CompletableFuture.complete()는 한 번만 호출할 수 있으며 후속 호출은 무시됩니다. 하지만 새로운 Future의 이전 값을 덮어쓰는 CompletableFuture.obtrudeValue(...)라는 백도어도 있으므로 주의해서 사용하시기 바랍니다.
Future 객체가 포함된 결과나 예외를 처리할 수 있다는 것을 알고 있기 때문에 신호가 실패하면 어떤 일이 발생하는지 보고 싶을 때가 있습니다. 몇 가지 예외를 추가로 전달하려면 CompletableFuture.completeExceptionally(ex)를 사용하거나 obtrudeException(ex)와 같은 더 강력한 메서드를 사용하여 이전 예외를 재정의할 수 있습니다. CompleteExceptionally()는 대기 중인 모든 클라이언트의 잠금도 해제하지만 이번에는 get()에서 예외가 발생합니다. get()에 대해 말하자면, 오류 처리에 미묘한 변화가 있는 CompletableFuture.join() 메서드도 있습니다. 그러나 전반적으로 그들은 모두 동일합니다. 마지막으로 차단하지 않지만 Future가 아직 완료되지 않은 경우 기본값을 반환하는 CompletableFuture.getNow(valueIfAbsent) 메서드가 있습니다. 이는 너무 오래 기다리고 싶지 않은 강력한 시스템을 구축할 때 매우 유용합니다.
마지막 정적 메서드는completeFuture(value)를 사용하여 완성된 Future 개체를 반환하는 것입니다. 이는 일부 어댑터 계층을 테스트하거나 작성할 때 매우 유용할 수 있습니다.
2. CompletableFuture 생성 및 획득
좋아요, CompletableFuture를 수동으로 생성하는 것이 유일한 옵션인가요? 불확실한. 일반 Future와 마찬가지로 기존 작업을 연결할 수 있으며 CompletableFuture는 팩토리 메서드를 사용합니다.
다음과 같이 코드 코드를 복사합니다.
static <U> CompletableFuture<U> 공급Async(공급업체<U> 공급업체);
static <U> CompletableFuture<U> 공급Async(Supplier<U> 공급자, 실행자 실행자);
static CompletableFuture<Void> runAsync(Runnable 실행 가능);
static CompletableFuture<Void> runAsync(실행 가능 실행 가능, 실행 실행자);
매개 변수가 없는 메서드 Executor는...Async로 끝나고 CompletableFuture 클래스의 대부분 메서드에 적용되는 ForkJoinPool.commonPool()(JDK8에 도입된 전역 공통 풀)을 사용합니다. runAsync()는 이해하기 쉽습니다. Runnable이 필요하므로 Runnable이 값을 반환하지 않으므로 CompletableFuture<Void>를 반환합니다. 비동기 작업을 처리하고 결과를 반환해야 하는 경우 Supply<U>를 사용하세요.
다음과 같이 코드 코드를 복사합니다.
final CompletableFuture<String> 미래 = CompletableFuture.supplyAsync(new Supply<String>() {
@보수
공개 문자열 get() {
//...오랜 실행...
"42"를 반환합니다.
}
}, 실행자);
하지만 Java 8에는 람다 표현식이 있다는 것을 잊지 마세요!
다음과 같이 코드 코드를 복사합니다.
finalCompletableFuture<String> 미래 = CompletableFuture.supplyAsync(() -> {
//...오랜 실행...
"42"를 반환합니다.
}, 실행자);
또는:
다음과 같이 코드 코드를 복사합니다.
최종 CompletableFuture<String> 미래 =
CompletableFuture.supplyAsync(() -> longRunningTask(params), 실행자);
이 글은 람다에 관한 글은 아니지만, 나는 람다 표현식을 꽤 자주 사용한다.
3. CompletableFuture(thenApply)에 대한 변환 및 작업
CompletableFuture가 Future보다 낫다고 했는데 왜 그런지 모르시나요? 간단히 말해서 CompletableFuture는 원자이자 요소이기 때문입니다. 내가 한 말이 도움이 되지 않았나요? Scala와 JavaScript를 사용하면 future가 완료될 때 비동기 콜백을 등록할 수 있으며 준비가 될 때까지 기다리거나 차단할 필요가 없습니다. 간단히 말해서, 이 함수를 실행하면 결과가 나타납니다. 또한 이러한 기능을 쌓고 여러 future를 결합하는 등의 작업을 수행할 수 있습니다. 예를 들어 문자열에서 정수로 변환하면 연관 없이 CompletableFuture에서 CompletableFuture<Integer로 변환할 수 있습니다. 이는 thenApply()를 통해 수행됩니다.
다음과 같이 코드 코드를 복사합니다.
<U> CompletableFuture<U> thenApply(Function<? super T,? 확장 U> fn);
<U> CompletableFuture<U> thenApplyAsync(Function<? super T,? 확장 U> fn);
<U> CompletableFuture<U> thenApplyAsync(Function<? super T,?extends U> fn, 실행기 실행기);<p></p>
<p>언급한 대로... Async 버전은 CompletableFuture에 대한 대부분의 작업을 제공하므로 이후 섹션에서는 건너뛰겠습니다. 첫 번째 메서드는 future가 완료된 동일한 스레드에서 메서드를 호출하는 반면, 나머지 두 메서드는 서로 다른 스레드 풀에서 비동기적으로 호출한다는 점을 기억하세요.
thenApply()의 작업 흐름을 살펴보겠습니다:</p>
<p><미리>
CompletableFuture<String> f1 = //...
CompletableFuture<Integer> f2 = f1.thenApply(Integer::parseInt);
CompletableFuture<Double> f3 = f2.thenApply(r -> r * r * Math.PI);
</p>
또는 성명서에서:
다음과 같이 코드 코드를 복사합니다.
CompletableFuture<Double> f3 =
f1.thenApply(Integer::parseInt).thenApply(r -> r * r * Math.PI);
여기서는 String에서 Integer, Double로 시퀀스가 변환되는 것을 볼 수 있습니다. 그러나 가장 중요한 것은 이러한 변환이 즉시 실행되거나 중지되지 않는다는 것입니다. 이러한 변환은 즉시 실행되거나 중지되지 않습니다. 그들은 단지 원래의 f1이 완료되었을 때 실행했던 프로그램을 기억합니다. 특정 변환에 시간이 많이 걸리는 경우 자체 실행기를 제공하여 비동기식으로 실행할 수 있습니다. 이 작업은 Scala의 단항 맵과 동일합니다.
4. 완성된 코드 실행(thenAccept/thenRun)
다음과 같이 코드 코드를 복사합니다.
CompletableFuture<Void> thenAccept(Consumer<? super T> block);
CompletableFuture<Void> thenRun(실행 가능한 작업);
향후 파이프라인에는 두 가지 일반적인 "최종" 단계 방법이 있습니다. 미래의 값을 사용할 때 준비됩니다. thenAccept()가 최종 값을 제공하면 thenRun은 값을 계산할 방법조차 없는 Runnable을 실행합니다. 예를 들어:
다음과 같이 코드 코드를 복사합니다.
future.thenAcceptAsync(dbl -> log.debug("결과: {}", dbl), 실행자);
log.debug("계속");
...비동기 변수는 암시적 실행자와 명시적 실행자의 두 가지 방법으로도 사용할 수 있으며 이 방법은 그다지 강조하지 않겠습니다.
thenAccept()/thenRun() 메서드는 차단하지 않습니다(명시적인 실행자가 없더라도). 이는 미래에 연결할 때 일정 기간 동안 실행되는 이벤트 리스너/핸들러와 같습니다. 미래가 아직 완료되지 않았음에도 불구하고 "계속 중" 메시지가 즉시 나타납니다.
5. 단일 CompletableFuture의 오류 처리
지금까지 우리는 계산 결과에 대해서만 논의했습니다. 예외는 어떻습니까? 비동기적으로 처리할 수 있나요? 틀림없이!
다음과 같이 코드 코드를 복사합니다.
CompletableFuture<String> 안전 =
future.Exceptionally(ex -> "문제가 있습니다: " + ex.getMessage());
Exceptionly()가 함수를 수락하면 원래 future가 호출되어 예외가 발생합니다. 우리는 이 예외를 Future 유형과 호환되는 값으로 변환하여 복구할 수 있는 기회를 갖게 될 것입니다. safeFurther 변환은 더 이상 예외를 발생시키지 않지만 대신 기능을 제공하는 함수에서 문자열 값을 반환합니다.
보다 유연한 접근 방식은 올바른 결과나 예외를 수신하는 함수를 handler()에서 받아들이는 것입니다.
다음과 같이 코드 코드를 복사합니다.
CompletableFuture<Integer> safe = future.handle((ok, ex) -> {
if (ok != null) {
return Integer.parseInt(ok);
} 또 다른 {
log.warn("문제", ex);
-1을 반환합니다.
}
});
handler()는 항상 호출되며 결과와 예외는 null이 아닙니다. 이는 원스톱 만능 전략입니다.
6. 두 개의 CompletableFuture를 함께 결합
비동기 프로세스 중 하나인 CompletableFuture는 훌륭하지만 이러한 여러 future가 다양한 방식으로 결합될 때 얼마나 강력한지 실제로 보여줍니다.
7. 이 두 future를 결합(링크)합니다(thenCompose()).
때때로 미래의 값(준비된 경우)을 실행하고 싶지만 이 함수는 미래도 반환합니다. CompletableFuture는 CompletableFuture<CompletableFuture>와 비교하여 함수 결과가 이제 최상위 future로 사용되어야 한다는 것을 이해할 만큼 유연합니다. thenCompose() 메소드는 Scala의 flatMap과 동일합니다.
다음과 같이 코드 코드를 복사합니다.
<U> CompletableFuture<U> thenCompose(Function<? super T,CompletableFuture<U>> fn);
...Async 변형도 사용할 수 있습니다. 다음 예제에서는 generateRelevance() 메서드를 적용하면 CompletableFuture가 반환됩니다.
다음과 같이 코드 코드를 복사합니다.
CompletableFuture<Document> docFuture = //...
CompletableFuture<CompletableFuture<Double>> f =
docFuture.thenApply(this::calculateRelevance);
CompletableFuture<Double> 관련성Future =
docFuture.thenCompose(this::calculateRelevance);
//...
private CompletableFuture<Double> 계산Relevance(Document doc) //...
thenCompose()는 중간 단계를 차단하거나 기다리지 않고도 강력한 비동기 파이프라인을 구축할 수 있는 중요한 방법입니다.
8. 두 미래의 전환값(thenCombine())
thenCompose()를 사용하여 다른 thenCombine에 의존하는 future를 연결하면 둘 다 완료되면 두 개의 독립적인 future를 결합합니다.
다음과 같이 코드 코드를 복사합니다.
<U,V> CompletableFuture<V> thenCombine(CompletableFuture<? 확장 U> 기타, BiFunction<? super T,? super U,? 확장 V> fn)
...두 개의 CompletableFuture(하나는 Customer를 로드하고 다른 하나는 최근 Shop을 로드)가 있다고 가정하면 비동기 변수도 사용할 수 있습니다. 그들은 서로 완전히 독립적이지만 완료되면 해당 값을 사용하여 경로를 계산하려고 합니다. 다음은 박탈 가능한 예입니다.
다음과 같이 코드 코드를 복사합니다.
CompletableFuture<Customer> customerFuture = loadCustomerDetails(123);
CompletableFuture<Shop> shopFuture = closeShop();
CompletableFuture<Route> RouteFuture =
customerFuture.thenCombine(shopFuture, (고객, 상점) -> findRoute(고객, 상점));
//...
개인 경로 findRoute(고객 고객, 상점 상점) //...
Java 8에서는 this::findRoute 메소드에 대한 참조를 (cust, shop) -> findRoute(cust, shop)으로 간단히 바꿀 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
customerFuture.thenCombine(shopFuture, this::findRoute);
아시다시피 customerFuture와 shopFuture가 있습니다. 그런 다음 RouteFuture는 이를 래핑하고 완료될 때까지 "기다립니다". 준비가 되면 모든 결과를 결합하기 위해 제공한 함수(findRoute())를 실행합니다. 이 RouteFuture는 두 가지 기본 Future가 완료되고 findRoute()도 완료되면 완료됩니다.
9. 모든 CompletableFutures가 완료될 때까지 기다립니다.
이 두 결과를 연결하는 새로운 CompletableFuture를 생성하는 대신 완료 시 알림을 받으려면 thenAcceptBoth()/runAfterBoth() 일련의 메서드를 사용할 수 있습니다(...Async 변수도 사용 가능). thenAccept() 및 thenRun()과 유사하게 작동하지만 하나가 아닌 두 개의 future를 기다립니다.
다음과 같이 코드 코드를 복사합니다.
<U> CompletableFuture<Void> thenAcceptBoth(CompletableFuture<? 확장 U> 기타, BiConsumer<? super T,? super U> 블록)
CompletableFuture<Void> runAfterBoth(CompletableFuture<?> 기타, 실행 가능한 작업)
위의 예를 상상해 보세요. 새로운 CompletableFuture를 생성하는 대신 일부 이벤트를 보내거나 GUI를 즉시 새로 고치기를 원합니다. 이는 쉽게 달성할 수 있습니다: thenAcceptBoth():
다음과 같이 코드 코드를 복사합니다.
customerFuture.thenAcceptBoth(shopFuture, (고객, 상점) -> {
최종 경로 경로 = findRoute(cust, shop);
//경로를 사용하여 GUI 새로 고침
});
내가 틀렸기를 바라지만, 어떤 사람들은 스스로에게 이렇게 질문할 수도 있습니다. 왜 나는 이 두 가지 미래를 단순히 차단할 수 없는 걸까? 좋다:
다음과 같이 코드 코드를 복사합니다.
Future<Customer> customerFuture = loadCustomerDetails(123);
Future<Shop> shopFuture = closeShop();
findRoute(customerFuture.get(), shopFuture.get());
물론 그렇게 할 수 있습니다. 그러나 가장 중요한 점은 CompletableFuture가 비동기성을 허용한다는 점입니다. 이는 결과를 차단하고 간절히 기다리는 것이 아니라 이벤트 중심 프로그래밍 모델입니다. 따라서 기능적으로 위의 두 코드 부분은 동일하지만 후자는 실행하기 위해 스레드를 차지할 필요가 없습니다.
10. 첫 번째 CompletableFuture가 작업을 완료할 때까지 기다립니다.
또 다른 흥미로운 점은 CompletableFutureAPI가 첫 번째(전체가 아닌) 미래가 완료될 때까지 기다릴 수 있다는 것입니다. 이는 동일한 유형의 두 작업 결과가 있을 때 응답 시간에만 관심이 있고 어떤 작업도 우선순위를 갖지 않을 때 매우 편리합니다. API 메소드(...비동기 변수도 사용 가능):
다음과 같이 코드 코드를 복사합니다.
CompletableFuture<Void> acceptEither(CompletableFuture<? 확장 T> 기타, 소비자<? super T> 블록)
CompletableFuture<Void> runAfterEither(CompletableFuture<?> other, 실행 가능한 작업)
예를 들어, 통합할 수 있는 두 개의 시스템이 있습니다. 하나는 평균 응답 시간이 더 작지만 표준 편차가 높고, 다른 하나는 일반적으로 느리지만 예측 가능성이 더 높습니다. 두 가지 측면(성능 및 예측 가능성)을 최대한 활용하려면 두 시스템을 동시에 호출하고 먼저 끝나는 시스템을 기다릴 수 있습니다. 일반적으로 이것이 첫 번째 시스템이 되지만 진행 속도가 느려지면 두 번째 시스템이 허용되는 시간 내에 완료될 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
CompletableFuture<String> fast = fetchFast();
CompletableFuture<String> 예측 가능 = fetchPredictously();
fast.acceptEither(예측 가능, s -> {
System.out.println("결과: " + s);
});
s는 fetchFast() 또는 fetchPredictously()에서 얻은 문자열을 나타냅니다. 우리는 알 필요도 없고 신경 쓸 필요도 없습니다.
11. 첫 번째 시스템을 완전히 전환
applyToEither()는 acceptEither()의 전신으로 간주됩니다. 두 future가 완료되려고 할 때 후자는 단순히 일부 코드 조각을 호출하고 applyToEither()는 새로운 future를 반환합니다. 이 두 개의 초기 미래가 완료되면 새로운 미래도 완료됩니다. API는 다소 유사합니다(...비동기 변수도 사용 가능).
다음과 같이 코드를 복사합니다.<U> CompletableFuture<U> applyToEither(CompletableFuture<? extends T> other, Function<? super T,U> fn)
이 추가 fn 함수는 첫 번째 future가 호출될 때 완료될 수 있습니다. 이 특수 메서드의 목적이 무엇인지 잘 모르겠습니다. 결국 fast.applyToEither(predictable).thenApply(fn)를 사용할 수 있습니다. 이 API만 사용하면 되지만 애플리케이션에 추가 기능이 실제로 필요하지 않으므로 간단히 Function.identity() 자리 표시자를 사용하겠습니다.
다음과 같이 코드 코드를 복사합니다.
CompletableFuture<String> fast = fetchFast();
CompletableFuture<String> 예측 가능 = fetchPredictously();
CompletableFuture<String> firstDone =
fast.applyToEither(예측 가능, Function.<String>identity());
처음으로 완성된 Future를 실행할 수 있습니다. 클라이언트의 관점에서 볼 때 두 미래는 실제로 firstDone 뒤에 숨겨져 있습니다. 클라이언트는 미래가 완료될 때까지 기다리고 applyToEither()를 사용하여 처음 두 작업이 완료되면 클라이언트에 알립니다.
12. 다양한 조합이 가능한 CompletableFuture
이제 우리는 두 future가 완료(thenCombine() 사용)되고 첫 번째 future가 완료(applyToEither())될 때까지 기다리는 방법을 알았습니다. 하지만 원하는 수의 미래로 확장할 수 있습니까? 실제로 정적 도우미 메서드를 사용하세요.
다음과 같이 코드 코드를 복사합니다.
정적 CompletableFuture<Void< allOf(CompletableFuture<?<... cfs)
정적 CompletableFuture<Object< anyOf(CompletableFuture<?<... cfs)
allOf()는 미래의 배열을 사용하고 모든 잠재적인 미래가 완료되면 (모든 장애물을 기다리는) 미래를 반환합니다. 반면에 anyOf()는 가장 빠른 잠재적 미래를 기다릴 것입니다. 반환된 미래의 일반적인 유형을 살펴보십시오. 다음 기사에서는 이 문제에 중점을 둘 것입니다.
요약
우리는 전체 CompletableFuture API를 탐색했습니다. 나는 이것이 무적일 것이라고 확신하므로 다음 기사에서는 CompletableFuture 메소드와 Java 8 람다 표현식을 사용하는 또 다른 간단한 웹 크롤러의 구현을 살펴볼 것입니다. 또한 CompletableFuture도 살펴볼 것입니다.