מדריך java.util.concurrent.Future

1. סקירה כללית

במאמר זה, אנו הולכים ללמוד על עתיד. ממשק שקיים מאז Java 1.5 ויכול להיות שימושי למדי בעבודה עם שיחות אסינכרוניות ועיבוד במקביל.

2. יצירה עתיד

במילים פשוטות, ה עתיד class מייצג תוצאה עתידית של חישוב אסינכרוני - תוצאה שתופיע בסופו של דבר ב- עתיד לאחר סיום העיבוד.

בואו נראה איך כותבים שיטות שיוצרות ומחזירות a עתיד למשל.

שיטות ריצה ארוכות הן מועמדות טובות לעיבוד אסינכרוני ול- עתיד מִמְשָׁק. זה מאפשר לנו לבצע תהליך אחר בזמן שאנו ממתינים למשימה שנמצאת בתוכה עתיד להשלים.

כמה דוגמאות לפעולות שתמנפו את האופי הסינכרוני של עתיד הם:

  • תהליכים אינטנסיביים חישוביים (חישובים מתמטיים ומדעיים)
  • מניפולציות במבני נתונים גדולים (ביג דאטה)
  • שיחות בשיטה מרוחקת (הורדת קבצים, גרוטאות HTML, שירותי אינטרנט).

2.1. יישום עתיד עם FutureTask

לדוגמא שלנו, אנו הולכים ליצור מחלקה פשוטה מאוד המחשבת את הריבוע של מספר שלם. זה בהחלט לא מתאים לקטגוריית השיטות "לאורך זמן", אבל אנחנו הולכים לשים Thread.sleep () התקשר אליו כדי להשלים את שנייה אחת:

מחלקה ציבורית SquareCalculator {מבצע ExecutorService פרטי = Executors.newSingleThreadExecutor (); חישוב עתיד ציבורי (קלט שלם) {return executor.submit (() -> {Thread.sleep (1000); קלט החזר * קלט;}); }}

פיסת הקוד שמבצעת בפועל את החישוב כלולה בתוך ה- שִׂיחָה() שיטה, המסופקת כביטוי למבדה. כפי שאתה יכול לראות אין שום דבר מיוחד בזה, למעט ה- לִישׁוֹן() שיחה שהוזכרה קודם.

זה נהיה מעניין יותר כאשר אנו מפנים את תשומת ליבנו לשימוש ב ניתן להתקשר ו שירות ExecutorService.

ניתן להתקשר הוא ממשק המייצג משימה שמחזירה תוצאה ויש לה יחידה שִׂיחָה() שיטה. הנה, יצרנו מופע שלו באמצעות ביטוי למבדה.

יצירת מופע של ניתן להתקשר לא לוקח אותנו לשום מקום, אנחנו עדיין צריכים להעביר את המקרה הזה למוציא לפועל שידאג להתחיל את המשימה בשרשור חדש ולהחזיר לנו את היקר עתיד לְהִתְנַגֵד. זה המקום שירות ExecutorService מגיע ב.

ישנן כמה דרכים בהן אנו יכולים להשיג שירות ExecutorService למשל, רובם ניתנים לפי מחלקת כלי עזר מוציאים להורג ' שיטות מפעל סטטיות. בדוגמה זו השתמשנו בבסיסי newSingleThreadExecutor (), שנותן לנו שירות ExecutorService מסוגל להתמודד עם חוט אחד בכל פעם.

ברגע שיש לנו שירות ExecutorService להתנגד, אנחנו רק צריכים להתקשר שלח() עוברים את שלנו ניתן להתקשר כוויכוח. שלח() ידאג להתחיל במשימה ולהחזיר א FutureTask אובייקט, שהוא יישום של עתיד מִמְשָׁק.

3. צורכים עתיד

עד כאן למדנו כיצד ליצור מופע של עתיד.

בחלק זה נלמד כיצד לעבוד עם מופע זה על ידי בחינת כל השיטות הנכללות בהן עתידה- API.

3.1. באמצעות נעשה() ו לקבל() להשיג תוצאות

עכשיו אנחנו צריכים להתקשר לחשב() והשתמש בחזרה עתיד כדי להשיג את התוצאה מספר שלם. שתי שיטות מה- עתיד API יעזור לנו במשימה זו.

Future.isDone () אומר לנו אם המבצע סיים לעבד את המשימה. אם המשימה תושלם, היא תחזור נָכוֹן אחרת, הוא חוזר שֶׁקֶר.

השיטה שמחזירה את התוצאה בפועל מהחישוב היא Future.get (). שימו לב ששיטה זו חוסמת את הביצוע עד לסיום המשימה, אך בדוגמה שלנו, זה לא יהיה בעיה מכיוון שנבדוק תחילה אם המשימה הושלמה על ידי התקשרות נעשה().

על ידי שימוש בשתי שיטות אלה אנו יכולים להריץ קוד אחר בזמן שאנחנו מחכים לסיום המשימה העיקרית:

עתיד עתידי = מחשבון כיכר חדש (). חשב (10); בעוד (! future.isDone ()) {System.out.println ("מחשב ..."); Thread.sleep (300); } תוצאה שלמה = future.get ();

בדוגמה זו אנו כותבים הודעה פשוטה על הפלט כדי להודיע ​​למשתמש שהתוכנית מבצעת את החישוב.

השיטה לקבל() יחסום את הביצוע עד לסיום המשימה. אבל אנחנו לא צריכים לדאוג לזה מכיוון שהדוגמה שלנו רק מגיעה לנקודה שבה לקבל() נקרא לאחר שווידאת שהמשימה הסתיימה. אז בתרחיש זה, future.get () תמיד יחזור מיד.

ראוי להזכיר זאת לקבל() יש גרסה עמוסה שלוקחת פסק זמן ו- TimeUnit כטיעונים:

תוצאה שלמה = future.get (500, TimeUnit.MILLISECONDS);

ההבדל בין קבל (ארוך, TimeUnit) ו לקבל(), הוא שהראשון יזרוק א פסק זמן פסק זמן אם המשימה לא חוזרת לפני פרק הזמן הקצוב שצוין.

3.2. ביטול א עתיד With לְבַטֵל()

נניח שהפעלנו משימה, אך משום מה כבר לא אכפת לנו מהתוצאה. אנחנו יכולים להשתמש Future.cancel (בוליאני) לומר למוציא לפועל להפסיק את הפעולה ולקטוע את השרשור הבסיסי שלה:

עתיד עתידי = מחשבון כיכר חדש (). חשב (4); בוליאני בוטל = future.cancel (נכון);

המקרה שלנו של עתיד מהקוד לעיל לעולם לא ישלים את פעולתו. למעשה, אם ננסה להתקשר לקבל() מאותו מקרה, לאחר השיחה אל לְבַטֵל()התוצאה תהיה ביטול חריג. Future.is בוטל () יגיד לנו אם א עתיד כבר בוטל. זה יכול להיות די שימושי כדי למנוע קבלת ביטול חריג.

יתכן שקריאה ל לְבַטֵל() נכשל. במקרה כזה, הערך שהוחזר יהיה שֶׁקֶר. שים לב ש לְבַטֵל() לוקח בוליאני ערך כוויכוח - זה שולט אם יש להפריע לשרשור שמבצע את המשימה הזו או לא.

4. יותר רב-הברגה עם פְּתִיל בריכות

הנוכחי שלנו שירות ExecutorService הוא משורשר בודד מאז שהוא הושג עם Executors.newSingleThreadExecutor. כדי להדגיש את "החוט היחיד" הזה, בוא נפעיל שני חישובים בו זמנית:

SquareCalculator squareCalculator = SquareCalculator חדש (); עתיד עתיד 1 = squareCalculator.calculate (10); עתיד עתיד 2 = squareCalculator.calculate (100); בעוד (! (future1.isDone () && future2.isDone ())) {System.out.println (String.format ("future1 הוא% s ו- future2 הוא% s", future1.isDone ()? "נעשה": "לא נעשה", future2.isDone ()? "נעשה": "לא נעשה")); Thread.sleep (300); } תוצאה שלמה 1 = future1.get (); תוצאה שלמה 2 = future2.get (); System.out.println (result1 + "ו-" + result2); squareCalculator.shutdown ();

בואו ננתח את הפלט עבור קוד זה:

חישוב ריבוע עבור: 10 עתיד 1 לא נעשה ועתיד 2 לא נעשה עתיד 1 לא נעשה ועתיד 2 לא נעשה עתיד 1 לא נעשה ועתיד 2 לא נעשה עתיד 1 לא נעשה ועתיד 2 לא נעשה חישוב ריבוע עבור: 100 עתיד 1 נעשה ו עתיד 2 לא נעשה עתיד 1 נעשה ועתיד 2 לא נעשה עתיד 1 נעשה ועתיד 2 לא נעשה 100 ו 10000

ברור שהתהליך אינו מקביל. שים לב כיצד המשימה השנייה מתחילה רק לאחר השלמת המשימה הראשונה, מה שגורם לכל התהליך להימשך כ -2 שניות.

כדי להפוך את התוכנית שלנו לרב-הברגה עלינו להשתמש בטעם אחר של שירות ExecutorService. בואו נראה כיצד ההתנהגות של הדוגמה שלנו משתנה אם אנו משתמשים במאגר פתילים, המסופק על ידי שיטת המפעל Executors.newFixedThreadPool ():

מחלקה ציבורית SquareCalculator {מבצע ExecutorService פרטי = Executors.newFixedThreadPool (2); // ...}

עם שינוי פשוט שלנו מחשבון מרובע מחלקה עכשיו יש לנו מבצע שמסוגל להשתמש בשני שרשורים בו זמנית.

אם נפעיל שוב את אותו קוד לקוח, נקבל את הפלט הבא:

חישוב ריבוע עבור: 10 חישוב ריבוע עבור: 100 עתיד 1 לא נעשה ועתיד 2 לא נעשה עתיד 1 לא נעשה ועתיד 2 לא נעשה עתיד 1 לא נעשה ועתיד 2 לא נעשה עתיד 1 לא נעשה ועתיד 2 לא נעשה 100 ו 10000

זה נראה הרבה יותר טוב עכשיו. שים לב כיצד שתי המשימות מתחילות ומסתיימות בהרצה בו זמנית, וכל התהליך לוקח בערך שנייה אחת להשלים.

ישנן שיטות מפעל אחרות בהן ניתן להשתמש כדי ליצור מאגרי חוטים, כמו Executors.newCachedThreadPool () שמשתמשים בשימוש בעבר פְּתִילכאשר הם זמינים, ו Executors.newScheduledThreadPool () המתזמן פקודות להפעלה לאחר עיכוב נתון.

למידע נוסף אודות שירות ExecutorServiceקרא את המאמר שלנו המוקדש לנושא.

5. סקירה כללית של ForkJoinTask

ForkJoinTask הוא מחלקה מופשטת אשר מיישמת עתיד ומסוגל להריץ מספר רב של משימות שמתארחות על ידי מספר קטן של שרשורים בפועל ForkJoinPool.

בחלק זה נסקור במהירות את המאפיינים העיקריים של ForkJoinPool. לקבלת מדריך מקיף בנושא, עיין במדריך למזלג / הצטרף למסגרת ב- Java.

ואז המאפיין העיקרי של א ForkJoinTask הוא שלרוב יוליד משימות משנה חדשות כחלק מהעבודה הנדרשת להשלמת המשימה העיקרית שלה. זה מייצר משימות חדשות על ידי התקשרות מזלג() וזה אוסף את כל התוצאות עם לְהִצְטַרֵף(), ובכך שם הכיתה.

ישנם שני שיעורים מופשטים המיישמים ForkJoinTask: משימת רקורסיביות שמחזיר ערך עם השלמתו, ו רקורסיבית אקשן שלא מחזיר דבר. כפי שמשתמע מהשמות, שיעורים אלה אמורים לשמש למשימות רקורסיביות, כמו למשל ניווט במערכת הקבצים או חישוב מתמטי מורכב.

בואו נרחיב את הדוגמה הקודמת שלנו כדי ליצור מחלקה בהינתן מספר שלם, יחשב את ריבועי הסכום על כל מרכיביו הפקטוריאליים. כך, למשל, אם אנו מעבירים את המספר 4 למחשבון שלנו, עלינו לקבל את התוצאה מהסכום של 4² + 3² + 2² + 1² שהוא 30.

קודם כל, עלינו ליצור יישום קונקרטי של משימת רקורסיביות וליישם את שלה לְחַשֵׁב() שיטה. זה המקום בו נכתוב את ההיגיון העסקי שלנו:

מחלקה ציבורית FactorialSquareCalculator מרחיב את RecursiveTask {Private Integer n; FactorialSquareCalculator ציבורי (מספר שלם n) {this.n = n; } @Override מוגן מחשב שלם () {if (n <= 1) {return n; } מחשבון FactorialSquareCalculator = FactorialSquareCalculator חדש (n - 1); calculator.fork (); החזר n * n + calculator.join (); }}

שימו לב כיצד אנו משיגים רקורסיביות על ידי יצירת מופע חדש של FactorialSquareCalculator בְּתוֹך לְחַשֵׁב(). על ידי התקשרות מזלג(), שיטה שאינה חוסמת, אנו שואלים ForkJoinPool ליזום ביצוע משימת משנה זו.

ה לְהִצְטַרֵף() השיטה תחזיר את התוצאה מאותו חישוב, אליה נוסיף את ריבוע המספר שאנו מבקרים כרגע.

עכשיו אנחנו רק צריכים ליצור ForkJoinPool לטיפול בביצוע וניהול חוטים:

ForkJoinPool forkJoinPool = ForkJoinPool חדש (); מחשבון FactorialSquareCalculator = FactorialSquareCalculator חדש (10); forkJoinPool.execute (מחשבון);

6. מסקנה

במאמר זה הייתה לנו מבט מקיף על ה- עתיד ממשק, מבקר בכל שיטותיו. למדנו גם כיצד למנף את העוצמה של מאגרי חוטים להפעלת פעולות מקבילות מרובות. השיטות העיקריות מה- ForkJoinTask מעמד, מזלג() ו לְהִצְטַרֵף() כוסו בקצרה גם כן.

יש לנו הרבה מאמרים מעולים אחרים על פעולות מקבילות ואסינכרוניות ב- Java. להלן שלוש מהן שקשורות קשר הדוק ל עתיד ממשק (חלקם כבר מוזכרים במאמר):

  • מדריך ל העתיד - יישום של עתיד עם הרבה תכונות נוספות שהוצגו ב- Java 8
  • מדריך למזלג / הצטרף למסגרת בג'אווה - מידע נוסף על ForkJoinTask סיקרנו בסעיף 5
  • מדריך ל- Java שירות ExecutorService - המוקדש ל שירות ExecutorService מִמְשָׁק

בדוק את קוד המקור המשמש במאמר זה במאגר GitHub שלנו.


$config[zx-auto] not found$config[zx-overlay] not found