JavaRush /בלוג Java /Random-HE /אתה לא יכול להרוס את ג'אווה עם שרשור: חלק IV - ניתן להתקש...
Viacheslav
רָמָה

אתה לא יכול להרוס את ג'אווה עם שרשור: חלק IV - ניתן להתקשרות, עתיד וחברים

פורסם בקבוצה

מבוא

כבר בדקנו כיצד נוצרים שרשורים בחלק הראשון . בואו נזכור שוב. אתה לא יכול לקלקל את ג'אווה עם שרשור: חלק IV - ניתן להתקשרות, עתיד וחברים - 1שרשור הוא 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. מהתרגום כבר ברורה מטרתו: זהו שלב מסוים של חישוב כלשהו. מבוא קצר לנושא ניתן למצוא בסקירה הכללית " מבוא לשלב השלמה ולעתיד להשלים ". בואו ניגש ישר לעניין. בואו נסתכל על רשימת השיטות הסטטיות הזמינות כדי לעזור לנו להתחיל: אתה לא יכול לקלקל את ג'אווה עם שרשור: חלק IV - ניתן להתקשרות, עתיד וחברים - 2להלן האפשרויות לשימוש בהן:
import java.util.concurrent.CompletableFuture;
public class App {
    public static void main(String []args) throws Exception {
        // CompletableFuture уже содержащий результат
        CompletableFuture<String> completed;
        completed = CompletableFuture.completedFuture("Просто meaning");
        // CompletableFuture, запускающий (run) новый поток с Runnable, поэтому он Void
        CompletableFuture<Void> voidCompletableFuture;
        voidCompletableFuture = CompletableFuture.runAsync(() -> {
            System.out.println("run " + Thread.currentThread().getName());
        });
        // CompletableFuture, запускающий новый поток, результат которого возьмём у Supplier
        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 computation
        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: אתה לא יכול להרוס את ג'אווה עם שרשור: חלק IV - ניתן להתקשרות, עתיד וחברים - 3שיטות אלה מקבלות חלופה CompletableStageויתבצעו על זו CompletableStageשתבוצע ראשונה. ואני רוצה לסיים את הסקירה הזו עם עוד תכונה מעניינת אחת CompletableFuture- טיפול בשגיאות.
CompletableFuture.completedFuture(2L)
				 .thenApply((a) -> {
					throw new IllegalStateException("error");
				 }).thenApply((a) -> 3L)
				 //.exceptionally(ex -> 0L)
				 .thenAccept(val -> System.out.println(val));
הקוד הזה לא יעשה כלום, כי... נזרק חריג ושום דבר לא יקרה. אבל אם נבטל הערות exceptionally, אז נגדיר את ההתנהגות. CompletableFutureאני ממליץ גם לצפות בסרטון הבא בנושא זה : לעניות דעתי, הסרטונים האלה הם מהוויזואליים ביותר באינטרנט. צריך להיות ברור מהם איך הכל עובד, איזה ארסנל יש לנו ולמה כל זה נחוץ.

סיכום

אני מקווה שעכשיו ברור כיצד ניתן להשתמש באשכולות כדי לאחזר חישובים לאחר שחושבו. חומר נוסף: #ויאצ'סלב
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION