Pemrograman Asynchronous di Java

1. Ikhtisar

Dengan meningkatnya permintaan untuk menulis kode non-pemblokiran, kami membutuhkan cara untuk mengeksekusi kode secara asinkron.

Dalam tutorial ini, kita akan melihat beberapa cara untuk mencapai pemrograman asinkron di Java. Juga, kami akan menjelajahi beberapa pustaka Java yang menyediakan solusi out-of-the-box.

2. Pemrograman Asynchronous di Java

2.1. Benang

Kami dapat membuat utas baru untuk melakukan operasi apa pun secara asinkron. Dengan dirilisnya ekspresi lambda di Java 8, itu lebih bersih dan lebih mudah dibaca.

Mari buat utas baru yang menghitung dan mencetak faktorial sebuah angka:

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

2.2. FutureTask

Sejak Java 5, antarmuka Future menyediakan cara untuk melakukan operasi asinkron menggunakan FutureTask .

Kita bisa menggunakan metode submit ExecutorService untuk melakukan tugas secara asinkron dan mengembalikan instance FutureTask .

Jadi, mari kita cari faktorial sebuah bilangan:

ExecutorService threadpool = Executors.newCachedThreadPool(); Future futureTask = threadpool.submit(() -> factorial(number)); while (!futureTask.isDone()) { System.out.println("FutureTask is not finished yet..."); } long result = futureTask.get(); threadpool.shutdown();

Di sini, kami telah menggunakan metode isDone yang disediakan oleh antarmuka Future untuk memeriksa apakah tugas telah selesai. Setelah selesai, kita bisa mengambil hasilnya menggunakan metode get .

2.3. CompletableFuture

Java 8 memperkenalkan CompletableFuture dengan kombinasi Future dan CompletionStage . Ini menyediakan berbagai metode seperti supplyAsync , runAsync , dan thenApplyAsync untuk pemrograman asynchronous.

Jadi, mari gunakan CompletableFuture sebagai pengganti FutureTask untuk menemukan faktorial sebuah bilangan:

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

Kami tidak perlu menggunakan ExecutorService secara eksplisit. The CompletableFuture internal menggunakan ForkJoinPool untuk menangani tugas secara asynchronous . Karenanya, itu membuat kode kita jauh lebih bersih.

3. Jambu biji

Guava menyediakan kelas ListenableFuture untuk melakukan operasi asinkron.

Pertama, kami akan menambahkan dependensi jambu Maven terbaru :

 com.google.guava guava 28.2-jre 

Kemudian, mari kita cari faktorial sebuah angka menggunakan ListenableFuture :

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

Di sini, kelas MoreExecutors menyediakan instance kelas ListeningExecutorService . Kemudian, metode ListeningExecutorService.submit melakukan tugas secara asinkron dan mengembalikan instance ListenableFuture .

Jambu biji juga memiliki Futures kelas yang menyediakan metode seperti submitAsync , scheduleAsync , dan transformAsync untuk rantai ListenableFutures mirip dengan CompletableFuture.

Misalnya, mari kita lihat cara menggunakan Futures.submitAsync sebagai pengganti metode ListeningExecutorService.submit :

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

Di sini, metode submitAsync memerlukan argumen AsyncCallable , yang dibuat menggunakan kelas Callables .

Selain itu, kelas Futures menyediakan metode addCallback untuk mendaftarkan callback berhasil dan gagal:

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

4. EA Async

Electronic Arts menghadirkan fitur async-await dari .NET ke ekosistem Java melalui pustaka ea-async .

Perpustakaan memungkinkan penulisan kode asinkron (non-pemblokiran) secara berurutan. Oleh karena itu, ini membuat pemrograman asinkron lebih mudah dan diskalakan secara alami.

Pertama, kami akan menambahkan dependensi ea-async Maven terbaru ke pom.xml :

 com.ea.async ea-async 1.2.3 

Kemudian, mari ubah kode CompletableFuture yang telah dibahas sebelumnya dengan menggunakan metode await yang disediakan oleh kelas Async EA :

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

Di sini, kami membuat panggilan ke metode Async.init di blok statis untuk menginisialisasi instrumentasi runtime Async .

Instrumentasi Async mengubah kode pada waktu proses dan menulis ulang panggilan ke metode await , agar berperilaku serupa dengan menggunakan rangkaian CompletableFuture .

Oleh karena itu, panggilan ke metode await mirip dengan memanggil Future.join.

Kita dapat menggunakan parameter - javaagent JVM untuk instrumentasi waktu kompilasi. Ini adalah alternatif dari metode Async.init :

java -javaagent:ea-async-1.2.3.jar -cp  

Mari kita periksa contoh lain dari penulisan kode asinkron secara berurutan.

Pertama, kita akan melakukan beberapa operasi rantai secara asinkron menggunakan metode komposisi seperti thenComposeAsync dan thenAcceptAsync dari kelas CompletableFuture :

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

Then, we can transform the code using EA's Async.await():

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

The implementation resembles the sequential blocking code. However, the await method doesn't block the code.

As discussed, all calls to the await method will be rewritten by the Async instrumentation to work similarly to the Future.join method.

So, once the asynchronous execution of the hello method is finished, the Future result is passed to the mergeWorld method. Then, the result is passed to the last execution using the CompletableFuture.runAsync method.

5. Cactoos

Cactoos is a Java library based on object-oriented principles.

It is an alternative to Google Guava and Apache Commons that provides common objects for performing various operations.

First, let's add the latest cactoos Maven dependency:

 org.cactoos cactoos 0.43 

The library provides an Async class for asynchronous operations.

So, we can find the factorial of a number using the instance of Cactoos's Async class:

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

Here, the apply method executes the operation using the ExecutorService.submit method and returns an instance of the Future interface.

Similarly, the Async class has the exec method that provides the same feature without a return value.

Note: the Cactoos library is in the initial stages of development and may not be appropriate for production use yet.

6. Jcabi-Aspects

Jcabi-Aspects provides the @Async annotation for asynchronous programming through AspectJ AOP aspects.

First, let's add the latest jcabi-aspects Maven dependency:

 com.jcabi jcabi-aspects 0.22.6  

The jcabi-aspects library requires AspectJ runtime support. So, we'll add the aspectjrt Maven dependency:

 org.aspectj aspectjrt 1.9.5  

Next, we'll add the jcabi-maven-plugin plugin that weaves the binaries with AspectJ aspects. The plugin provides the ajc goal that does all the work for us:

 com.jcabi jcabi-maven-plugin 0.14.1    ajc      org.aspectj aspectjtools 1.9.1   org.aspectj aspectjweaver 1.9.1   

So, we're all set to use the AOP aspects for asynchronous programming:

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

When we compile the code, the library will inject AOP advice in place of the @Async annotation through AspectJ weaving, for the asynchronous execution of the factorialUsingAspect method.

So, let's compile the class using the Maven command:

mvn install

The output from the jcabi-maven-plugin may look like:

 --- 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)

We can verify if our class is woven correctly by checking the logs in the jcabi-ajc.log file, generated by the Maven plugin:

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))

Then, we'll run the class as a simple Java application, and the output will look like:

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): '[email protected][Completed normally]' in 44.64µs

So, we can see a new daemon thread jcabi-async is created by the library that performed the task asynchronously.

Similarly, the logging is enabled by the @Loggable annotation provided by the library.

7. Conclusion

In this article, we've seen a few ways of asynchronous programming in Java.

To begin with, we explored Java's in-built features like FutureTask and CompletableFuture for asynchronous programming. Then, we've seen a few libraries like EA Async and Cactoos with out-of-the-box solutions.

Also, we examined the support of performing tasks asynchronously using Guava's ListenableFuture and Futures classes. Last, we explored the jcabi-AspectJ library that provides AOP features through its @Async annotation for asynchronous method calls.

Seperti biasa, semua implementasi kode tersedia di GitHub.