معرفی
در
قسمت اول
، نحوه ایجاد موضوعات را بررسی کردیم. بیایید یک بار دیگر به یاد بیاوریم.
![بهتر با هم: جاوا و کلاس Thread. قسمت چهارم - تماس پذیر، آینده و دوستان - 1]()
یک رشته با کلاس Thread نمایش داده می شود که
run()
متد آن فراخوانی می شود. پس بیایید از
کامپایلر جاوا آنلاین Tutorialspoint
استفاده کنیم و کد زیر را اجرا کنیم:
public class HelloWorld {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Hello World");
};
new Thread(task).start();
}
}
آیا این تنها گزینه برای شروع یک کار در یک موضوع است؟
java.util.concurrent.Callable
معلوم شد که
java.lang.Runnable
برادری به نام
java.util.concurrent.Callable
دارد که با جاوا 1.5 وارد دنیا شده است. چه تفاوت هایی دارند؟ اگر به Javadoc برای این رابط دقت کنید، می بینیم که برخلاف
Runnable
, رابط جدید
call()
متدی را اعلام می کند که نتیجه را برمی گرداند. همچنین، Exception را به طور پیش فرض می اندازد. یعنی ما را از
try-catch
بلاک کردن استثناهای بررسی شده نجات می دهد. بد نیست، درست است؟ اکنون به جای این کار یک وظیفه جدید داریم
Runnable
:
Callable task = () -> {
return "Hello, World!";
};
اما با آن چه کنیم؟ چرا به وظیفه ای نیاز داریم که روی رشته ای اجرا شود که نتیجه را برمی گرداند؟ بدیهی است که برای هر اقدامی که در آینده انجام شود، انتظار داریم نتیجه آن اقدامات را در آینده دریافت کنیم. و ما یک رابط با نام مربوطه داریم:
java.util.concurrent.Future
java.util.concurrent.Future
رابط
java.util.concurrent.Future
یک API را برای کار با وظایفی تعریف می کند که نتایج آنها را در آینده دریافت می کنیم: روش هایی برای به دست آوردن نتیجه و روش هایی برای بررسی وضعیت. در رابطه با
Future
، ما علاقه مند به پیاده سازی آن در کلاس
java.util.concurrent.FutureTask
هستیم . این "وظیفه" است که در اجرا خواهد شد
Future
. چیزی که این پیاده سازی را جالب تر می کند این است که Runnable را نیز پیاده سازی می کند. شما می توانید این را نوعی آداپتور بین مدل قدیمی کار با وظایف روی رشته ها و مدل جدید (به معنای جدید در جاوا 1.5) در نظر بگیرید. به عنوان مثال:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class HelloWorld {
public static void main(String[] args) throws Exception {
Callable task = () -> {
return "Hello, World!";
};
FutureTask<String> future = new FutureTask<>(task);
new Thread(future).start();
System.out.println(future.get());
}
}
همانطور که از مثال می بینید، ما از
get
روش برای به دست آوردن نتیجه از کار استفاده می کنیم.
توجه داشته باشید:وقتی نتیجه را با استفاده از
get()
روش بدست آورید، اجرا همزمان می شود! فکر می کنید چه مکانیزمی در اینجا استفاده خواهد شد؟ درست است، هیچ بلوک همگام سازی وجود ندارد.
به همین دلیل است که ما WAITING را
در JVisualVM بهعنوان a
monitor
یا نمیبینیم
wait
، بلکه بهعنوان
park()
روش آشنا (زیرا
LockSupport
مکانیزم در حال استفاده است).
رابط های کاربردی
در مرحله بعد، ما در مورد کلاس های جاوا 1.8 صحبت خواهیم کرد، بنابراین بهتر است یک مقدمه کوتاه ارائه دهیم. به کد زیر نگاه کنید:
Supplier<String> supplier = new Supplier<String>() {
@Override
public String get() {
return "String";
}
};
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
Function<String, Integer> converter = new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.valueOf(s);
}
};
تعداد زیادی کد اضافی، نمی گویید؟ هر یک از کلاس های اعلام شده یک تابع را انجام می دهد، اما ما از یک دسته کد پشتیبانی اضافی برای تعریف آن استفاده می کنیم. و توسعه دهندگان جاوا اینگونه فکر می کردند. بر این اساس، آنها مجموعهای از «واسطهای عملکردی» (
@FunctionalInterface
) را معرفی کردند و تصمیم گرفتند که اکنون خود جاوا «تفکر» را انجام دهد، و فقط چیزهای مهم را برای ما باقی میگذارد که نگرانشان باشیم:
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
یک
Supplier
لوازم هیچ پارامتری ندارد، اما چیزی را برمی گرداند. به این ترتیب چیزها را تامین می کند. A
Consumer
مصرف می کند. چیزی را به عنوان ورودی (استدلال) می گیرد و با آن کاری انجام می دهد. بحث آن چیزی است که مصرف می کند. سپس ما نیز داریم
Function
. ورودی ها (استدلال ها) را می گیرد، کاری انجام می دهد و چیزی را برمی گرداند. می بینید که ما به طور فعال از ژنریک استفاده می کنیم. اگر مطمئن نیستید، میتوانید با خواندن «
عمومی در جاوا: نحوه استفاده از براکتهای زاویهدار در عمل
» تجدید نظر کنید.
CompletableFuture
زمان گذشت و کلاس جدیدی به نام
CompletableFuture
در جاوا 1.8 ظاهر شد. این
Future
رابط را پیاده سازی می کند، یعنی وظایف ما در آینده تکمیل می شود و می توانیم
get()
برای دریافت نتیجه تماس بگیریم. اما این
CompletionStage
رابط را نیز پیاده سازی می کند. این نام گویای همه چیز است: این مرحله مشخصی از برخی محاسبات است. مقدمه کوتاهی درباره موضوع را می توان در بررسی اینجا یافت: مقدمه CompletionStage و CompletableFuture. بریم سر اصل مطلب بیایید به لیستی از روشهای استاتیک موجود که به ما در شروع کار کمک میکنند نگاهی بیندازیم:
![بهتر با هم: جاوا و کلاس Thread. قسمت چهارم - تماس پذیر، آینده و دوستان - 2]()
در اینجا گزینههایی برای استفاده از آنها وجود دارد:
import java.util.concurrent.CompletableFuture;
public class App {
public static void main(String[] args) throws Exception {
CompletableFuture<String> completed;
completed = CompletableFuture.completedFuture("Just a value");
CompletableFuture<Void> voidCompletableFuture;
voidCompletableFuture = CompletableFuture.runAsync(() -> {
System.out.println("run " + Thread.currentThread().getName());
});
CompletableFuture<String> supplier;
supplier = CompletableFuture.supplyAsync(() -> {
System.out.println("supply " + Thread.currentThread().getName());
return "Value";
});
}
}
اگر این کد را اجرا کنیم، خواهیم دید که ایجاد یک
CompletableFuture
شامل راه اندازی یک خط لوله کامل نیز می شود. بنابراین، با شباهت خاصی به SteamAPI از Java8، اینجاست که تفاوت بین این رویکردها را پیدا می کنیم. مثلا:
List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
System.out.println("Executed");
return value.toUpperCase();
});
این نمونه ای از استریم API جاوا 8 است. اگر این کد را اجرا کنید، خواهید دید که "Executed" نمایش داده نمی شود. به عبارت دیگر، هنگامی که یک جریان در جاوا ایجاد می شود، جریان بلافاصله شروع نمی شود. در عوض، منتظر می ماند تا کسی ارزشی از آن بخواهد. اما
CompletableFuture
بلافاصله شروع به اجرای خط لوله می کند، بدون اینکه منتظر کسی باشد که از آن مقدار بخواهد. من فکر می کنم درک این مهم است. S O، ما یک
CompletableFuture
. چگونه می توانیم خط لوله (یا زنجیره) بسازیم و چه مکانیزم هایی داریم؟ آن رابط های کاربردی را که قبلاً در مورد آنها نوشتیم به یاد بیاورید.
- ما a داریم
Function
که یک A می گیرد و یک B برمی گرداند. یک متد دارد: apply()
.
- ما a داریم
Consumer
که یک A می گیرد و چیزی بر نمی گرداند (Void). یک روش دارد: accept()
.
- ما داریم
Runnable
که روی موضوع اجرا می شود و هیچ چیزی را نمی گیرد و چیزی را برمی گرداند. یک روش دارد: run()
.
نکته بعدی که باید به خاطر بسپارید این است که از , و در کار خود
CompletableFuture
استفاده می کند . بر این اساس، همیشه می توانید بدانید که می توانید کارهای زیر را با :
Runnable
Consumers
Functions
CompletableFuture
public static void main(String[] args) throws Exception {
AtomicLong longValue = new AtomicLong(0);
Runnable task = () -> longValue.set(new Date().getTime());
Function<Long, Date> dateConverter = (longvalue) -> new Date(longvalue);
Consumer<Date> printer = date -> {
System.out.println(date);
System.out.flush();
};
CompletableFuture.runAsync(task)
.thenApply((v) -> longValue.get())
.thenApply(dateConverter)
.thenAccept(printer);
}
متدهای
thenRun()
،
thenApply()
و
thenAccept()
دارای نسخههای "Async" هستند. این بدان معنی است که این مراحل در یک موضوع متفاوت تکمیل می شود. این موضوع از یک استخر خاص گرفته خواهد شد - بنابراین ما از قبل نمی دانیم که آیا یک موضوع جدید یا قدیمی خواهد بود. همه چیز به این بستگی دارد که وظایف چقدر محاسباتی فشرده هستند. علاوه بر این روش ها، سه احتمال جالب دیگر نیز وجود دارد. برای وضوح، بیایید تصور کنیم که یک سرویس خاص داریم که نوعی پیام را از جایی دریافت می کند - و این زمان می برد:
public static class NewsService {
public static String getMessage() {
try {
Thread.currentThread().sleep(3000);
return "Message";
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
حال، بیایید نگاهی به توانایی های دیگری که
CompletableFuture
فراهم می کند بیاندازیم. ما می توانیم نتیجه a را
CompletableFuture
با نتیجه دیگری ترکیب کنیم
CompletableFuture
:
Supplier newsSupplier = () -> NewsService.getMessage();
CompletableFuture<String> reader = CompletableFuture.supplyAsync(newsSupplier);
CompletableFuture.completedFuture("!!")
.thenCombine(reader, (a, b) -> b + a)
.thenAccept(result -> System.out.println(result))
.get();
توجه داشته باشید که thread ها به طور پیش فرض رشته های شبح هستند، بنابراین برای وضوح، ما
get()
منتظر نتیجه هستیم. نه تنها می توانیم ترکیب کنیم
CompletableFutures
، بلکه می توانیم یک
CompletableFuture
:
CompletableFuture.completedFuture(2L)
.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
.thenAccept(result -> System.out.println(result));
در اینجا می خواهم توجه داشته باشم که از
CompletableFuture.completedFuture()
روش برای اختصار استفاده شد. این روش نخ جدیدی ایجاد نمی کند، بنابراین بقیه خط لوله در همان رشته ای که
completedFuture
فراخوانی شده بود اجرا می شود. یک روش هم وجود دارد
thenAcceptBoth()
. بسیار شبیه به است
accept()
، اما اگر
thenAccept()
a را بپذیرد
Consumer
، +
thenAcceptBoth()
دیگری را به عنوان ورودی می پذیرد، یعنی a که به جای یک منبع، 2 منبع می گیرد. قابلیت جالب دیگری نیز توسط متدهایی ارائه میشود که نام آن شامل کلمه "یا" میشود: این روشها یک جایگزین را میپذیرند و روی آن که ابتدا اجرا میشود اجرا میشوند. در نهایت، میخواهم این بررسی را با یکی دیگر از ویژگیهای جالب پایان دهم : مدیریت خطا.
CompletableStage
BiConsumer
consumer
![بهتر با هم: جاوا و کلاس Thread. قسمت چهارم - تماس پذیر، آینده و دوستان - 3]()
CompletableStage
CompletableStage
CompletableFuture
CompletableFuture.completedFuture(2L)
.thenApply((a) -> {
throw new IllegalStateException("error");
}).thenApply((a) -> 3L)
.thenAccept(val -> System.out.println(val));
این کد هیچ کاری انجام نمی دهد، زیرا یک استثنا وجود خواهد داشت و هیچ چیز دیگری اتفاق نمی افتد. اما با برداشتن اظهارنظر از عبارت " استثنایی " ، رفتار مورد انتظار را تعریف می کنیم. در مورد
CompletableFuture
, من همچنین توصیه می کنم ویدیو زیر را تماشا کنید:
به نظر حقیر من اینها جزو توضیحی ترین ویدیوهای اینترنت هستند. آنها باید روشن کنند که همه اینها چگونه کار می کند، چه ابزاری در دسترس داریم و چرا همه اینها مورد نیاز است.
نتیجه
امیدواریم، اکنون مشخص شده است که چگونه می توانید از موضوعات برای دریافت محاسبات پس از تکمیل آنها استفاده کنید. مواد اضافی:
بهتر با هم: جاوا و کلاس Thread. قسمت اول - موضوعات اجرا
بهتر با هم: جاوا و کلاس Thread. بخش دوم - همگام سازی
بهتر با هم: کلاس جاوا و Thread. بخش سوم - تعامل
بهتر با هم: کلاس جاوا و Thread. قسمت پنجم - Executor، ThreadPool، Fork/Join
Better together: Java و کلاس Thread. قسمت ششم - آتش دور شوید!
GO TO FULL VERSION