معرفی
قبلاً در قسمت اول نحوه ایجاد موضوعات را بررسی کرده ایم . دوباره به یاد بیاوریم.
یک رشته
Thread
چیزی است که در آن اجرا می شود
run
، بنابراین بیایید از
کامپایلر آنلاین tutorialspoint java استفاده کرده و کد زیر را اجرا کنیم:
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!";
};
اما با آن چه باید کرد؟ چرا ما حتی به وظیفه ای نیاز داریم که روی رشته ای اجرا شود که نتیجه را برمی گرداند؟ بدیهی است که در آینده انتظار دریافت نتیجه اقداماتی را داریم که در آینده انجام خواهد شد. آینده در انگلیسی - Future. و یک رابط دقیقاً با همین نام وجود دارد:
java.util.concurrent.Future
java.util.concurrent.Future
رابط
java.util.concurrent.Future یک API را برای کار با وظایفی که ما قصد داریم نتایج آنها را در آینده بدست آوریم توصیف می کند: روش هایی برای به دست آوردن نتایج، روش هایی برای بررسی وضعیت. ما
Future
علاقه مند به اجرای آن
java.util.concurrent.FutureTask هستیم . یعنی
Task
این همان چیزی است که در اجرا خواهد شد
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
نتیجه مسئله را به دست می آوریم
task
.
(!)مهم، که در لحظه ای که نتیجه با استفاده از روش به دست می آید،
get
اجرا به صورت همزمان می شود. فکر می کنید در اینجا از چه مکانیزمی استفاده می شود؟ درست است، هیچ بلوک همگامسازی وجود ندارد - بنابراین ما
WAITING را در JVisualVM نه بهعنوان
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
- ارائه دهنده. هیچ پارامتری ندارد، اما چیزی را برمی گرداند، یعنی آن را تامین می کند.
Consumer
- مصرف كننده. چیزی را به عنوان ورودی (پارامتر s) می گیرد و با آن کاری انجام می دهد، یعنی چیزی را مصرف می کند. عملکرد دیگری نیز وجود دارد. چیزی را به عنوان ورودی (پارامتر) می گیرد
s
، کاری انجام می دهد و چیزی را برمی گرداند. همانطور که می بینیم، ژنریک ها به طور فعال مورد استفاده قرار می گیرند. اگر مطمئن نیستید، می توانید آنها را به خاطر بسپارید و "
نظریه ژنریک در جاوا یا نحوه قرار دادن پرانتز در عمل " را بخوانید.
CompletableFuture
با گذشت زمان، جاوا 1.8 کلاس جدیدی به نام
CompletableFuture
. این رابط را پیاده سازی می کند
Future
، به این معنی که مال ما
task
در آینده اجرا می شود و ما می توانیم اجرا کنیم
get
و نتیجه را بگیریم. اما برخی را نیز اجرا می کند
CompletionStage
. از ترجمه، هدف آن از قبل مشخص است: این مرحله خاصی از نوعی محاسبه است. مقدمه کوتاهی برای موضوع را می توان در نمای کلی "
مقدمه ای بر CompletionStage و CompletableFuture " یافت. یک راست بریم سر اصل مطلب. بیایید به لیستی از روشهای استاتیک موجود برای کمک به شروع کار نگاه کنیم:
در اینجا گزینههایی برای استفاده از آنها وجود دارد:
import java.util.concurrent.CompletableFuture;
public class App {
public static void main(String []args) throws Exception {
CompletableFuture<String> completed;
completed = CompletableFuture.completedFuture("Просто meaning");
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 "Значение";
});
}
}
اگر این کد را اجرا کنیم، خواهیم دید که ایجاد
CompletableFuture
شامل شروع کل زنجیره است. بنابراین، در حالی که شباهت هایی با SteamAPI از Java8 وجود دارد، این تفاوت بین این رویکردها است. مثلا:
List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
System.out.println("Executed");
return value.toUpperCase();
});
این نمونهای از Java 8 Stream Api است (میتوانید در اینجا «
راهنمای Java 8 Stream API در تصاویر و مثالها » درباره آن بیشتر بخوانید). اگر این کد را اجرا کنید،
Executed
نمایش داده نمی شود. یعنی هنگام ایجاد یک جریان در جاوا، جریان بلافاصله شروع نمی شود، بلکه منتظر می ماند تا مقداری از آن مورد نیاز باشد. اما
CompletableFuture
بدون منتظر ماندن برای درخواست مقدار محاسبه شده، زنجیره را برای اجرا شروع می کند. من فکر می کنم درک این موضوع مهم است. بنابراین ما CompletableFuture را داریم. چگونه می توانیم زنجیره ایجاد کنیم و چه وسیله ای داریم؟ بیایید در مورد رابط های کاربردی که قبلاً در مورد آنها نوشتیم به یاد بیاوریم.
- ما یک تابع (
Function
) داریم که A را می گیرد و B را برمی گرداند. این یک متد دارد - apply
(apply).
- ما یک مصرف کننده (
Consumer
) داریم که A را می پذیرد و چیزی بر نمی گرداند ( Void ). فقط یک روش دارد - accept
(قبول).
- ما کدی داریم که روی رشته ای اجرا می شود
Runnable
که قبول نمی کند یا باز نمی گردد. این یک روش دارد - run
(اجرا).
دومین چیزی که باید به خاطر بسپارید این است که در کار خود از مصرف کنندگان و عملکردها
CompletalbeFuture
استفاده می کند .
Runnable
با توجه به این، همیشه می توانید به یاد داشته باشید که
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
. می توانیم نتیجه را
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();
شایان ذکر است که بهطور پیشفرض رشتهها رشتههای شبح هستند، بنابراین برای وضوح
get
، ما منتظر نتیجه هستیم. و ما نه تنها می توانیم ترکیب (ترکیب)، بلکه برگردانیم
CompletableFuture
:
CompletableFuture.completedFuture(2L)
.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
.thenAccept(result -> System.out.println(result));
در اینجا می خواهم توجه داشته باشم که برای اختصار از روش استفاده شده است
CompletableFuture.completedFuture
. این روش نخ جدیدی ایجاد نمی کند، بنابراین بقیه زنجیره در همان رشته ای که در آن فراخوانی شده است اجرا می شود
completedFuture
. یک روش هم وجود دارد
thenAcceptBoth
. بسیار شبیه به است
accept
، اما اگر
thenAccept
قبول کند
consumer
، +
thenAcceptBoth
دیگری را به عنوان ورودی می پذیرد ، یعنی 2 منبع را به عنوان ورودی می پذیرد، نه یکی را. یک احتمال جالب دیگر با کلمه وجود دارد : این روش ها جایگزینی را می پذیرند و روی روشی که ابتدا اجرا می شود اجرا می شود. و من می خواهم این بررسی را با یک ویژگی جالب دیگر به پایان برسانم - مدیریت خطا.
CompletableStage
BiConsumer
consumer
Either
CompletableStage
CompletableStage
CompletableFuture
CompletableFuture.completedFuture(2L)
.thenApply((a) -> {
throw new IllegalStateException("error");
}).thenApply((a) -> 3L)
.thenAccept(val -> System.out.println(val));
این کد هیچ کاری انجام نمی دهد، زیرا ... یک استثنا پرتاب می شود و هیچ اتفاقی نمی افتد. اما اگر نظر را لغو کنیم
exceptionally
، رفتار را تعریف می کنیم.
CompletableFuture
همچنین توصیه می کنم ویدیوی زیر را در این زمینه تماشا کنید:
به نظر حقیر من، این ویدیوها از بصری ترین ویدیوها در اینترنت هستند. باید از آنها مشخص شود که همه چیز چگونه کار می کند، چه زرادخانه ای داریم و چرا به آن همه نیاز است.
نتیجه
امیدواریم اکنون مشخص شده باشد که چگونه می توان از رشته ها برای بازیابی محاسبات پس از محاسبه آنها استفاده کرد. مواد اضافی:
#ویاچسلاو
GO TO FULL VERSION