برنامه نویسی ناهمگام در جاوا — به زبان ساده
با توجه به رشد روزافزون تقاضا برای نوشتن کدهای غیر مسدودکننده، باید روشهایی برای اجرای ناهمگام کد پیدا کنیم. در این راهنما چندین روش برای دستیابی به برنامه نویسی ناهمگام در جاوا را مورد بررسی قرار میدهیم. همچنین چند کتابخانه جاوا که راهحلهای آمادهای به این منظور ارائه میکنند را بررسی خواهیم کرد.
برنامه نویسی ناهمگام در جاوا
در این بخش روشهای برنامهنویسی ناهمگام را در جاوا معرفی میکنیم.
Thread
امکان ایجاد یک نخ جدید در جاوا برای اجرای ناهمگام یک عملیات وجود دارد. با معرفی عبارتهای لامبدا در جاوا 8 این کار اینک به روشی تمیزتر و با خوانایی بیشتر نیز انجام مییابد.
در ادامه یک نخ جدید میسازیم که فاکتوریل یک عدد را محاسبه کرده و پرینت میکند:
1int number = 20;
2Thread newThread = new Thread(() -> {
3 System.out.println("Factorial of " + number + " is: " + factorial(number));
4});
5newThread.start();
FutureTask
از جاوا 5 به بعد اینترفیس Future روشی برای اجرای عملیات ناهمگام با استفاده از FutureTask ارائه کرده است. به این ترتیب میتوانیم از متد submit در xecutorService برای اجرای ناهمگام یک کار بهره بگیریم و وهلهای از FutureTask را بازگشت دهیم. از این روش برای یافتن فاکتوریل یک عدد استفاده میکنیم:
1ExecutorService threadpool = Executors.newCachedThreadPool();
2Future<Long> futureTask = threadpool.submit(() -> factorial(number));
3
4while (!futureTask.isDone()) {
5 System.out.println("FutureTask is not finished yet...");
6}
7long result = futureTask.get();
8
9threadpool.shutdown();
در این کد از متد isDone که از سوی اینترفیس Future ارائه شده، استفاده میکنیم تا ببینیم آیا کار مورد نظر پایان یافته است یا نه. زمانی که کار پایان یافت، میتوانیم نتیجه را با استفاده از متد get بازیابی کنیم.
CompletableFuture
جاوا 8 امکانی به نام CompletableFuture معرفی کرده است که با Future و CompletionStage ترکیب میشود. این قابلیت متدهایی مانند supplyAsync ،runAsync و thenApplyAsync برای برنامهنویسی ناهمگام ارائه میکند.
در کد زیر از CompletableFuture به جای FutureTask برای یافتن فاکتوریل یک عدد کمک میگیریم:
1CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(() -> factorial(number));
2while (!completableFuture.isDone()) {
3 System.out.println("CompletableFuture is not finished yet...");
4}
5long result = completableFuture.get();
در این جا لزومی به استفاده صریح از ExecutorService وجود ندارد. CompletableFuture به صورت داخلی از ForkJoinPool برای مدیریت ناهمگام کارها بهره میگیرد. از این رو کد به مقدار زیادی تمیزتر میشود.
Guava
Guava کلاسی به نام ListenableFuture برای اجرای عملیات ناهمگام ارائه کرده است. ابتدا آخرین وابستگی Maven مربوط به Guava را اضافه میکنیم:
1<dependency>
2 <groupId>com.google.guava</groupId>
3 <artifactId>guava</artifactId>
4 <version>28.2-jre</version>
5</dependency>
سپس فاکتوریل عدد را با استفاده از ListenableFuture مییابیم:
1ExecutorService threadpool = Executors.newCachedThreadPool();
2ListeningExecutorService service = MoreExecutors.listeningDecorator(threadpool);
3ListenableFuture<Long> guavaFuture = (ListenableFuture<Long>) service.submit(()-> factorial(number));
4long result = guavaFuture.get();
در این کد کلاس MoreExecutors وهلهای از کلاس ListeningExecutorService ارائه میکند. سپس متد ListeningExecutorService.submit وظیفه مورد نظر را به صورت ناهمگام اجرا میکند و وهلهای از ListenableFuture بازگشت میدهد. Guava یک کلاس به نام Futures نیز دارد که متدهایی مانند submitAsync ،scheduleAsync و transformAsync برای اتصال به ListenableFutures مانند CompletableFuture ارائه میکند.
برای نمونه در کد زیر شیوه استفاده از Futures.submitAsync را به جای متد ListeningExecutorService.submit میبینیم:
1ListeningExecutorService service = MoreExecutors.listeningDecorator(threadpool);
2AsyncCallable<Long> asyncCallable = Callables.asAsyncCallable(new Callable<Long>() {
3 public Long call() {
4 return factorial(number);
5 }
6}, service);
7ListenableFuture<Long> guavaFuture = Futures.submitAsync(asyncCallable, service);
در کد فوق متد submitAsync نیازمند یک آرگومان AsyncCallable است که با استفاده از کلاس Callables ساخته شده است. به علاوه کلاس Futures متد addCallback را برای ثبت callback-های موفقیت و شکست ارائه میکند:
1Futures.addCallback(
2 factorialFuture,
3 new FutureCallback<Long>() {
4 public void onSuccess(Long factorial) {
5 System.out.println(factorial);
6 }
7 public void onFailure(Throwable thrown) {
8 thrown.getCause();
9 }
10 },
11 service);
EA Async
Electronic Arts قابلیت async-await را از طریق کتابخانه ea-async از .Net به اکوسیستم جاوا آورده است. این کتابخانه امکان نوشتن کد ناهمگام (غیر مسدودکننده) را به صورت ترتیبی فراهم ساخته است. از این رو برنامهنویسی ناهمگام آسانتر شده و به صورت طبیعی مقیاسپذیر میشود. ابتدا آخرین وابستگی Maven مربوط به ea-async را به فایل pom.xml اضافه کنید:
1<dependency>
2 <groupId>com.ea.async</groupId>
3 <artifactId>ea-async</artifactId>
4 <version>1.2.3</version>
5</dependency>
سپس کد CompletableFuture را که قبلاً مورد بررسی قرار دادیم با استفاده از متد await که از سوی کلاس Async ارائه شده است بازنویسی میکنیم:
1static {
2 Async.init();
3}
4
5public long factorialUsingEAAsync(int number) {
6 CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(() -> factorial(number));
7 long result = Async.await(completableFuture);
8}
در این کد یک فراخوانی به متد Async.init در بلوک static داریم تا تجهیزِ زمانِ اجرای Async را مقداردهی کنیم. تجهیز Async موجب میشود که در زمان اجرا تبدیل شود و فراخوانی به متد await بازنویسی میشود تا رفتاری مشابه با استفاده از زنجیرهای CompletableFuture داشته باشد.
از این رو فراخوانی به متد await مشابه فراخوانی Future.join است. میتوانیم از پارامتر javaagent– در JVM برای تجهیز زمان کامپایل استفاده کنیم. این روش جایگزینی برای متد Async.init محسوب میشود:
1java -javaagent:ea-async-1.2.3.jar -cp <claspath> <MainClass>
در ادامه مثال دیگری از نوشتن کد ناهمگام به روش ترتیبی را بررسی میکنیم. ابتدا چند عملیات زنجیرهای را به صورت ناهمگام با استفاده از متدهای ترکیبی مانند thenComposeAsync و thenAcceptAsync از کلاس CompletableFuture اجرا میکنیم:
1CompletableFuture<Void> completableFuture = hello()
2 .thenComposeAsync(hello -> mergeWorld(hello))
3 .thenAcceptAsync(helloWorld -> print(helloWorld))
4 .exceptionally(throwable -> {
5 System.out.println(throwable.getCause());
6 return null;
7 });
8completableFuture.get();
سپس میتوانیم کد را با استفاده از ()Async.await مربوط به EA تبدیل کنیم:
1try {
2 String hello = await(hello());
3 String helloWorld = await(mergeWorld(hello));
4 await(CompletableFuture.runAsync(() -> print(helloWorld)));
5} catch (Exception e) {
6 e.printStackTrace();
7}
این پیادهسازی مشابه کد مسدودسازی ترتیبی است. اما متد await موجب میشود که کد مسدود نشود. چنانکه اشاره کردیم همه فراخوانیها به متد await از سوی تجهیز Async بازنویسی میشوند تا مشابه متد کار کنند.
بنابراین زمانی که اجرای ناهمگام متد hello پایان یابد، نتیجه Future به متد mergeWorld ارسال میشود. سپس نتیجه با استفاده از متد CompletableFuture.runAsync به آخرین اجرا فرستاده میشود.
Cactoos
Cactoos یک کتابخانه جاوا است بر مبنای مفاهیم شیءگرایی است.
کتابخانه کاکتوس جایگزینی برای کتابخانه Guava گوگل و Commons آپاچی محسوب میشود که اشیای مشترکی برای اجرای عملیات مختلف ارائه میکند. ابتدا آخرین وابستگی Maven مربوط به cactoos را اضافه میکنیم:
1<dependency>
2 <groupId>org.cactoos</groupId>
3 <artifactId>cactoos</artifactId>
4 <version>0.43</version>
5</dependency>
این کتابخانه یک کلاس Async برای عملیات ناهمگام ارائه میکند. بنابراین میتوانیم فاکتوریل یک عدد را با استفاده از وهلهای از کلاس Async کاکتوس پیدا کنیم:
1Async<Integer, Long> asyncFunction = new Async<Integer, Long>(input -> factorial(input));
2Future<Long> asyncFuture = asyncFunction.apply(number);
3long result = asyncFuture.get();
در این کد، متد apply عملیات را با استفاده از متد ExecutorService.submit اجرا میکند و یک وهله از اینترفیس Future بازگشت میدهد. به طور مشابه کلاس Async متد exec را دارد که همین قابلیت را بدون مقدار بازگشتی ارائه میکند.
نکته: کتابخانه کاکتوس در مراحل اولیه توسعه قرار دارد و ممکن است هنوز برای استفاده در محیط پروداکشن مناسب نباشد.
Jcabi-Aspects
Jcabi-Aspects یک حاشیهنویسی برای برنامهنویسی ناهمگام از طریق جنبههای «برنامهنویسی جنبهگرا» (AOP) در AspectJ ارائه میکند. ابتدا آخرین وابستگی Maven مربوط به jcabi-aspects را اضافه میکنیم:
1<dependency>
2 <groupId>com.jcabi</groupId>
3 <artifactId>jcabi-aspects</artifactId>
4 <version>0.22.6</version>
5</dependency>
کتابخانه jcabi-aspects نیازمند پشتیبانی زمان اجرای AspectJ است. بنابراین وابستگی Maven مربوط به aspectjrt را اضافه خواهیم کرد:
1<dependency>
2 <groupId>org.aspectj</groupId>
3 <artifactId>aspectjrt</artifactId>
4 <version>1.9.5</version>
5</dependency>
سپس پلاگین jcabi-maven-plugin را اضافه میکنیم که جنبههای AspectJ را به فایلهای باینری میافزاید. این پلاگین هدف ajc را ارائه میکند که همه کارهای مورد نظر ما را انجام میدهد:
1<plugin>
2 <groupId>com.jcabi</groupId>
3 <artifactId>jcabi-maven-plugin</artifactId>
4 <version>0.14.1</version>
5 <executions>
6 <execution>
7 <goals>
8 <goal>ajc</goal>
9 </goals>
10 </execution>
11 </executions>
12 <dependencies>
13 <dependency>
14 <groupId>org.aspectj</groupId>
15 <artifactId>aspectjtools</artifactId>
16 <version>1.9.1</version>
17 </dependency>
18 <dependency>
19 <groupId>org.aspectj</groupId>
20 <artifactId>aspectjweaver</artifactId>
21 <version>1.9.1</version>
22 </dependency>
23 </dependencies>
24</plugin>
بنابراین باید از جنبههای AOP برای برنامهنویسی ناهمگام بهره بگیریم:
1@Async
2@Loggable
3public Future<Long> factorialUsingAspect(int number) {
4 Future<Long> factorialFuture = CompletableFuture.completedFuture(factorial(number));
5 return factorialFuture;
6}
زمانی که کد کامپایل میشود، این کتابخانه روش AOP را به جای حاشیهنویسی Async@ از طریق بهکارگیری AspectJ تزریق میکند. به این ترتیب متد factorialUsingAspect به صورت ناهمگام اجرا میشود. بنابراین ابتدا کلاس را با استفاده از دستور Maven کامپایل میکنیم:
mvn install
خروجی پلاگین jcabi-maven- jcabi-maven-plugin به صورت زیر است:
1
2--- jcabi-maven-plugin:0.14.1:ajc (default) @ java-async ---
3[INFO] jcabi-aspects 0.18/55a5c13 started new daemon thread jcabi-loggable for watching of @Loggable annotated methods
4[INFO] Unwoven classes will be copied to /tutorials/java-async/target/unwoven
5[INFO] jcabi-aspects 0.18/55a5c13 started new daemon thread jcabi-cacheable for automated cleaning of expired @Cacheable values
6[INFO] ajc result: 10 file(s) processed, 0 pointcut(s) woven, 0 error(s), 0 warning(s)
میتوانیم با نگاه کردن به لاگها در فایل jcabi-ajc.log بررسی کنیم که آیا کلاس از سوی پلاگین Maben به درستی اجرا شده است یا نه:
1Join point 'method-execution(java.util.concurrent.Future
2com.baeldung.async.JavaAsync.factorialUsingJcabiAspect(int))'
3in Type 'com.baeldung.async.JavaAsync' (JavaAsync.java:158)
4advised by around advice from 'com.jcabi.aspects.aj.MethodAsyncRunner'
5(jcabi-aspects-0.22.6.jar!MethodAsyncRunner.class(from MethodAsyncRunner.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
بنابراین یک نخ daemon جدید به نام jcabi-async میبینیم که از سوی کتابخانه ایجاد شده و به صورت ناهمگام وظیفه را اجرا میکند.
سخن پایانی
در این مقاله چندین روش در مورد برنامهنویسی ناهمگام در جاوا را بررسی کردیم. در آغاز به بررسی ویژگیهای داخلی جاوا مانند FutureTask و CompletableFuture برای برنامهنویسی ناهمگام پرداختیم. سپس چند کتابخانه مانند EA Async و Cactoos را بررسی کردیم که راهحلهای آمادهای عرضه کردهاند.
همچنین به بررسی پشتیبانی از اجرای ناهمگام کارها با استفاده از کلاسهای ListenableFuture و Futures کتابخانه Guava پرداختیم. در نهایت کتابخانه jcabi-AspectJ را بررسی کردیم که قابلیتهای برنامهنویسی جنبهگرا را از طریق حاشیهنویسی Async@ برای اجرای متدهای ناهمگام ارائه میکند. همه کدهای مورد بررسی در این مقاله را میتوانید در این ریپوی گیتهاب (+) ببینید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای جاوا (Java)
- مجموعه آموزشهای برنامهنویسی
- گنجینه آموزشهای جاوا (Java)
- زبان برنامهنویسی جاوا (Java) — از صفر تا صد
- ساختمان داده Collection در جاوا — به زبان ساده
==