מבוא
כבר בדקנו כיצד נוצרים שרשורים
בחלק הראשון . בואו נזכור שוב.
שרשור הוא
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.ניתן להתקשר
מסתבר של-
java.lang.Runnable יש אח ושמו
java.util.concurrent.Callable והוא נולד ב-Java 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
. אפשר לראות בזה סוג של מתאם של המודל הישן של עבודה עם משימות ב-threads ושל הדגם החדש (חדש במובן שהוא הופיע ב-java 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
).
ממשקים פונקציונליים
בשלב הבא נדבר על שיעורים מ-Java 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);
}
};
יש הרבה קוד מיותר, לא? כל אחת מהמחלקות המוצהרות מבצעת פונקציה בודדת, אבל כדי לתאר אותה אנו משתמשים בחבורה של קוד עזר מיותר. וגם מפתחי Java חשבו כך. לכן, הם הציגו קבוצה של "ממשקים פונקציונליים" (
@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
), עושה משהו ומחזיר משהו. כפי שאנו רואים, נעשה שימוש פעיל בגנריקה. אם אינך בטוח, אתה יכול לזכור אותם ולקרוא את "
התיאוריה של גנריות ב-Java או כיצד ליישם סוגריים בפועל ."
CompletableFuture
ככל שחלף הזמן, Java 1.8 הציגה מחלקה חדשה בשם
CompletableFuture
. הוא מיישם את הממשק
Future
, כלומר שלנו
task
יבוצע בעתיד ונוכל לבצע
get
ולקבל את התוצאה. אבל הוא גם מיישם כמה
CompletionStage
. מהתרגום כבר ברורה מטרתו: זהו שלב מסוים של חישוב כלשהו. מבוא קצר לנושא ניתן למצוא בסקירה הכללית "
מבוא לשלב השלמה ולעתיד להשלים ". בואו ניגש ישר לעניין. בואו נסתכל על רשימת השיטות הסטטיות הזמינות כדי לעזור לנו להתחיל:
להלן האפשרויות לשימוש בהן:
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
(החל).
- יש לנו צרכן (
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
הוא מקבל עוד
CompletableStage
+ כקלט
BiConsumer
, כלומר
consumer
, שמקבל 2 מקורות כקלט, לא אחד. ישנה אפשרות מעניינת נוספת עם המילה
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