1. 개요

비 차단 코드 작성에 대한 수요가 증가함에 따라 코드를 비동기 적으로 실행할 수있는 방법이 필요합니다.

이 튜토리얼에서는 Java에서 비동기 프로그래밍 을 달성하는 몇 가지 방법을 살펴 보겠습니다 . 또한 즉시 사용 가능한 솔루션을 제공하는 몇 가지 Java 라이브러리를 살펴 보겠습니다.

2. 자바의 비동기 프로그래밍

2.1.

모든 작업을 비동기 적으로 수행하기 위해 새 스레드를 만들 수 있습니다. Java 8에서 람다 표현식 이 출시됨에 따라 더 깔끔하고 가독성이 향상되었습니다.

숫자의 계승을 계산하고 인쇄하는 새 스레드를 만들어 보겠습니다.

int number = 20;
Thread newThread = new Thread(() -> {
    System.out.println("Factorial of " + number + " is: " + factorial(number));
});
newThread.start();

2.2. FutureTask

Java 5부터 Future 인터페이스는 FutureTask를 사용하여 비동기 작업을 수행하는 방법을 제공합니다 .

ExecutorServicesubmit 메서드 를 사용하여 작업을 비동기 적으로 수행하고 FutureTask 인스턴스를 반환 할 수 있습니다.

자, 숫자의 계승을 찾아 봅시다 :

ExecutorService threadpool = Executors.newCachedThreadPool();
Future<Long> futureTask = threadpool.submit(() -> factorial(number));

while (!futureTask.isDone()) {
    System.out.println("FutureTask is not finished yet..."); 
} 
long result = futureTask.get(); 

threadpool.shutdown();

여기서는 Future 인터페이스에서 제공 하는 isDone 메서드를 사용 하여 작업이 완료되었는지 확인했습니다. 완료되면 get 메소드를 사용하여 결과를 검색 할 수 있습니다 .

2.3. 완료 가능 미래

Java 8 FutureCompletionStage 가 결합 CompletableFuture도입 했습니다 . 비동기 프로그래밍을 위해 supplyAsync , runAsyncthenApplyAsync같은 다양한 메서드를 제공합니다 .

따라서 숫자의 계승을 찾기 위해 FutureTask 대신 CompletableFuture사용합시다 .

CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(() -> factorial(number));
while (!completableFuture.isDone()) {
    System.out.println("CompletableFuture is not finished yet...");
}
long result = completableFuture.get();

ExecutorService를 명시 적으로 사용할 필요는 없습니다 . CompletableFuture는 내부적으로 사용 ForkJoinPool를 비동기 적으로 작업을 처리 할 수 . 따라서 코드가 훨씬 깔끔해집니다.

3. 구아바

Guava비동기 작업을 수행하기 위해 ListenableFuture 클래스를 제공합니다 .

먼저 최신 guava Maven 의존성을 추가합니다 .

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.2-jre</version>
</dependency>

그런 다음 ListenableFuture를 사용하여 숫자의 계승을 찾습니다 .

ExecutorService threadpool = Executors.newCachedThreadPool();
ListeningExecutorService service = MoreExecutors.listeningDecorator(threadpool);
ListenableFuture<Long> guavaFuture = (ListenableFuture<Long>) service.submit(()-> factorial(number));
long result = guavaFuture.get();

여기에서 MoreExecutors 클래스는 ListeningExecutorService 클래스 의 인스턴스를 제공합니다  . 그런 다음 ListeningExecutorService.submit  메서드는 작업을 비동기 적으로 수행하고 ListenableFuture 의 인스턴스를 반환합니다 .

Guava에는 CompletableFuture 와 유사한 ListenableFuture 를 연결하기 위해 submitAsync , scheduleAsynctransformAsync같은 메서드를 제공 하는 Futures 클래스 도 있습니다 .

예를 들어 ListeningExecutorService.submit  메서드  대신 Futures.submitAsync 를 사용하는 방법을 살펴 보겠습니다 .

ListeningExecutorService service = MoreExecutors.listeningDecorator(threadpool);
AsyncCallable<Long> asyncCallable = Callables.asAsyncCallable(new Callable<Long>() {
    public Long call() {
        return factorial(number);
    }
}, service);
ListenableFuture<Long> guavaFuture = Futures.submitAsync(asyncCallable, service);

여기서 submitAsync 메서드에는 Callables 클래스를 사용하여 생성 된 AsyncCallable 인수가 필요합니다 .

또한 Futures 클래스는 성공 및 실패 콜백을 등록하는 addCallback 메서드를 제공합니다 .

Futures.addCallback(
  factorialFuture,
  new FutureCallback<Long>() {
      public void onSuccess(Long factorial) {
          System.out.println(factorial);
      }
      public void onFailure(Throwable thrown) {
          thrown.getCause();
      }
  }, 
  service);

4. EA 비동기

Electronic Arts는 ea-async 라이브러리를 통해 .NET의 async-await 기능을 Java 생태계로 가져 왔습니다 .

라이브러리를 사용하면 비동기 (비 차단) 코드를 순차적으로 작성할 수 있습니다. 따라서 비동기 프로그래밍을 더 쉽게 만들고 자연스럽게 확장됩니다.

먼저 pom.xml에 최신 ea-async Maven 의존성을 추가합니다 .

<dependency>
    <groupId>com.ea.async</groupId>
    <artifactId>ea-async</artifactId>
    <version>1.2.3</version>
</dependency>

그런 다음 EA의 Async 클래스 에서 제공 하는 await 메서드 를 사용하여 앞에서 설명한 CompletableFuture 코드를 변환 해 보겠습니다 .

static { 
    Async.init(); 
}

public long factorialUsingEAAsync(int number) {
    CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(() -> factorial(number));
    long result = Async.await(completableFuture);
}

여기서는 정적 블록 Async.init  메서드를 호출 하여 Async 런타임 계측 을 초기화합니다 .

비동기 계측은 런타임에 코드를 변환하고 CompletableFuture 체인을 사용하는 것과 유사하게 동작하도록 await 메서드에 대한 호출을 다시 작성합니다 .

따라서,  받는 전화 AWAIT 방법은 호출과 유사 미래는 바로입니다.

컴파일 타임 인스 트루먼 테이션을 위해 javaagent JVM 매개 변수를 사용할 수 있습니다 . 이것은 Async.init 메서드 의 대안입니다 .

java -javaagent:ea-async-1.2.3.jar -cp <claspath> <MainClass>

비동기 코드를 순차적으로 작성하는 또 다른 예를 살펴 보겠습니다.

먼저 CompletableFuture 클래스 thenComposeAsyncthenAcceptAsync같은 컴포지션 메서드를 사용하여 몇 가지 체인 작업을 비동기 적으로 수행합니다 .

CompletableFuture<Void> completableFuture = hello()
  .thenComposeAsync(hello -> mergeWorld(hello))
  .thenAcceptAsync(helloWorld -> print(helloWorld))
  .exceptionally(throwable -> {
      System.out.println(throwable.getCause()); 
      return null;
  });
completableFuture.get();

그런 다음 EA의 Async.await ()를 사용하여 코드를 변환 할 수 있습니다 .

try {
    String hello = await(hello());
    String helloWorld = await(mergeWorld(hello));
    await(CompletableFuture.runAsync(() -> print(helloWorld)));
} catch (Exception e) {
    e.printStackTrace();
}

구현은 순차 차단 코드와 유사합니다. 그러나 await 메서드는 코드를 차단하지 않습니다.

설명했듯이 await 메서드 에 대한 모든 호출 Future.join 메서드 와 유사하게 작동 하도록 비동기 계측에 의해 다시 작성됩니다 .

따라서 hello 메서드 의 비동기 실행 이 완료되면 Future 결과가 mergeWorld 메서드에 전달됩니다 . 그런 다음 결과는 CompletableFuture.runAsync 메서드를 사용하여 마지막 실행으로 전달됩니다 .

5. 선인장

Cactoos는 객체 지향 원칙에 기반한 Java 라이브러리입니다.

다양한 작업을 수행하기위한 공통 객체를 제공하는 Google Guava 및 Apache Commons의 대안입니다.

먼저 최신 cactoos Maven 의존성을 추가해 보겠습니다 .

<dependency>
    <groupId>org.cactoos</groupId>
    <artifactId>cactoos</artifactId>
    <version>0.43</version>
</dependency>

라이브러리는 비동기 작업을위한 Async 클래스를 제공합니다 .

따라서 Cactoos의 Async 클래스 인스턴스를 사용하여 숫자의 계승을 찾을 수 있습니다 .

Async<Integer, Long> asyncFunction = new Async<Integer, Long>(input -> factorial(input));
Future<Long> asyncFuture = asyncFunction.apply(number);
long result = asyncFuture.get();

여기서, 적용 방법 실행하여 조작 ExecutorService.submit의  방법 및 복귀 인스턴스 미래 인터페이스 .

마찬가지로 Async 클래스에는 반환 값없이 동일한 기능을 제공하는 exec 메서드가 있습니다.

참고 : Cactoos 라이브러리는 개발 초기 단계에 있으며 아직 프로덕션 사용에 적합하지 않을 수 있습니다.

6. Jcabi-Aspects

Jcabi-Aspects는 AspectJ AOP 측면을 통해 비동기 프로그래밍을위한 @Async 어노테이션을 제공합니다 .

먼저 최신 jcabi-aspects Maven 의존성을 추가해 보겠습니다 .

<dependency>
    <groupId>com.jcabi</groupId>
    <artifactId>jcabi-aspects</artifactId>
    <version>0.22.6</version>
</dependency>

jcabi-측면의 라이브러리는 AspectJ의 런타임 지원이 필요합니다. 따라서 aspectjrt Maven 의존성을 추가합니다 .

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.5</version>
</dependency>

다음 으로 AspectJ 측면으로 바이너리를 짜는 jcabi-maven-plugin 플러그인을 추가합니다 . 플러그인은 우리를 위해 모든 작업을 수행 하는 ajc 목표를 제공합니다 .

<plugin>
    <groupId>com.jcabi</groupId>
    <artifactId>jcabi-maven-plugin</artifactId>
    <version>0.14.1</version>
    <executions>
        <execution>
            <goals>
                <goal>ajc</goal>
            </goals>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjtools</artifactId>
            <version>1.9.1</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.1</version>
        </dependency>
    </dependencies>
</plugin>

따라서 우리는 모두 비동기 프로그래밍에 AOP 측면을 사용하도록 설정되었습니다.

@Async
@Loggable
public Future<Long> factorialUsingAspect(int number) {
    Future<Long> factorialFuture = CompletableFuture.completedFuture(factorial(number));
    return factorialFuture;
}

코드를 컴파일 할 때 라이브러리는 factorialUsingAspect 메서드 의 비동기 실행을 위해 AspectJ weaving을 통해 @Async 어노테이션 대신 AOP 어드바이스를 삽입 합니다.

이제 Maven 명령을 사용하여 클래스를 컴파일 해 보겠습니다.

mvn install

jcabi-maven-plugin 의 출력 은 다음과 같습니다.

 --- jcabi-maven-plugin:0.14.1:ajc (default) @ java-async ---
[INFO] jcabi-aspects 0.18/55a5c13 started new daemon thread jcabi-loggable for watching of @Loggable annotated methods
[INFO] Unwoven classes will be copied to /tutorials/java-async/target/unwoven
[INFO] jcabi-aspects 0.18/55a5c13 started new daemon thread jcabi-cacheable for automated cleaning of expired @Cacheable values
[INFO] ajc result: 10 file(s) processed, 0 pointcut(s) woven, 0 error(s), 0 warning(s)

Maven 플러그인에 의해 생성 된 jcabi-ajc.log 파일 의 로그를 확인하여 클래스가 올바르게 짜여졌는지 확인할 수 있습니다 .

Join point 'method-execution(java.util.concurrent.Future 
com.baeldung.async.JavaAsync.factorialUsingJcabiAspect(int))' 
in Type 'com.baeldung.async.JavaAsync' (JavaAsync.java:158) 
advised by around advice from 'com.jcabi.aspects.aj.MethodAsyncRunner' 
(jcabi-aspects-0.22.6.jar!MethodAsyncRunner.class(from MethodAsyncRunner.java))

그런 다음 클래스를 간단한 Java 애플리케이션으로 실행하고 출력은 다음과 같습니다.

17:46:58.245 [main] INFO com.jcabi.aspects.aj.NamedThreads - 
jcabi-aspects 0.22.6/3f0a1f7 started new daemon thread jcabi-loggable for watching of @Loggable annotated methods
17:46:58.355 [main] INFO com.jcabi.aspects.aj.NamedThreads - 
jcabi-aspects 0.22.6/3f0a1f7 started new daemon thread jcabi-async for Asynchronous method execution
17:46:58.358 [jcabi-async] INFO com.baeldung.async.JavaAsync - 
#factorialUsingJcabiAspect(20): 'java.util.concurrent.CompletableFuture@14e2d7c1[Completed normally]' in 44.64µs

따라서 작업을 비동기 적으로 수행 한 라이브러리에 의해 새로운 데몬 스레드 jcabi-async 가 생성되는 것을 볼 수 있습니다 .

마찬가지로 로깅은 라이브러리에서 제공 하는 @Loggable 어노테이션에 의해 활성화됩니다 .

7. 결론

이 기사에서는 Java에서 비동기 프로그래밍의 몇 가지 방법을 살펴 보았습니다.

우선 비동기 프로그래밍을위한 FutureTaskCompletableFuture같은 Java의 내장 기능을 탐색했습니다 . 그런 다음 즉시 사용 가능한 솔루션을 갖춘 EA Async 및 Cactoos와 같은 몇 가지 라이브러리를 보았습니다.

또한 Guava의 ListenableFutureFutures 클래스를 사용하여 비동기식으로 작업을 수행하는 지원을 조사했습니다 . 마지막 으로 비동기 메서드 호출을위한 @Async 어노테이션을 통해 AOP 기능을 제공하는 jcabi-AspectJ 라이브러리를 살펴 보았습니다 .

평소처럼 모든 코드 구현은 GitHub에서 사용할 수  있습니다 .