המתן והודיע ​​() על שיטות ב- Java

1. הקדמה

במאמר זה נבחן את אחד המנגנונים הבסיסיים ביותר בג'אווה - סנכרון חוטים.

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

ונפתח אפליקציה פשוטה - בה נעסוק בבעיות במקביל, במטרה להבין טוב יותר לַחֲכוֹת() ו לְהוֹדִיעַ().

2. סנכרון חוטים בג'אווה

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

2.1. חסומים מוגנים בג'אווה

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

עם זאת בחשבון, נשתמש ב:

  • Object.wait () - להשעות חוט
  • Object.notify () - להעיר חוט

ניתן להבין זאת טוב יותר מהדיאגרמה הבאה המתארת ​​את מחזור החיים של a פְּתִיל:

שים לב שישנן דרכים רבות לשלוט על מחזור חיים זה; עם זאת, במאמר זה אנו נתמקד רק ב לַחֲכוֹת() ו לְהוֹדִיעַ().

3. ה לַחֲכוֹת() שיטה

במילים פשוטות, כשאנחנו מתקשרים חכה () - זה מכריח את החוט הנוכחי להמתין עד לפתיחת חוט אחר לְהוֹדִיעַ() אוֹ notifyAll () על אותו אובייקט.

לשם כך, על הברגה הנוכחית להיות הבעלים של צג האובייקט. על פי Javadocs, זה יכול לקרות כאשר:

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

שים לב שרק שרשור פעיל אחד יכול להחזיק צג של אובייקט בכל פעם.

זֶה לַחֲכוֹת() השיטה מגיעה עם שלוש חתימות עמוסות. בואו נסתכל על אלה.

3.1. לַחֲכוֹת()

ה לַחֲכוֹת() השיטה גורמת לשרשור הנוכחי להמתין ללא הגבלת זמן עד שמתחיל שרשור אחר לְהוֹדִיעַ() לאובייקט זה או notifyAll ().

3.2. המתן (פסק זמן ארוך)

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

שים לב שמתקשר חכה (0) זהה להתקשר לַחֲכוֹת().

3.3. המתן (פסק זמן רב, int ננו)

זוהי חתימה נוספת המספקת את אותה פונקציונליות, כאשר ההבדל היחיד הוא שנוכל לספק דיוק גבוה יותר.

תקופת פסק הזמן הכוללת (בשניות ננו) מחושבת כ- 1_000_000 * פסק זמן + ננו.

4. הודע () ו notifyAll ()

ה לְהוֹדִיעַ() השיטה משמשת להעיר חוטים שממתינים לגישה לצג של אובייקט זה.

ישנן שתי דרכים להודיע ​​על אשכולות המתנה.

4.1. לְהוֹדִיעַ()

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

מאז לְהוֹדִיעַ() מעיר חוט אקראי אחד וניתן להשתמש בו בכדי ליישם נעילה בלעדית, כאשר חוטים מבצעים משימות דומות, אך ברוב המקרים יהיה ניתן ליישם יותר notifyAll ().

4.2. notifyAll ()

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

החוטים שהתעוררו ישלימו בצורה הרגילה - כמו כל חוט אחר.

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

5. בעיית סנכרון שולח-מקלט

עכשיו, אחרי שאנחנו מבינים את היסודות, בואו נעבור פשוט שׁוֹלֵחַמַקְלֵט יישום - שיעשה שימוש ב- לַחֲכוֹת() ו לְהוֹדִיעַ() שיטות להגדרת סנכרון ביניהן:

  • ה שׁוֹלֵחַ אמור לשלוח חבילת נתונים אל מַקְלֵט
  • ה מַקְלֵט לא יכול לעבד את חבילת הנתונים עד שׁוֹלֵחַ מסיים לשלוח אותו
  • באופן דומה, ה שׁוֹלֵחַ אסור לנסות לשלוח חבילה נוספת אלא אם כן מַקְלֵט כבר עיבד את החבילה הקודמת

בואו ניצור קודם נתונים מחלקה המורכבת מהנתונים חֲבִילָה שיישלח מ שׁוֹלֵחַ ל מַקְלֵט. נשתמש לַחֲכוֹת() ו notifyAll () להגדרת סנכרון ביניהם:

מחלקה ציבורית נתונים {מנות מחרוזות פרטיות; // נכון אם המקלט צריך לחכות // שקר אם השולח צריך להמתין העברה בוליאנית פרטית = נכון; שליחת חלל ריקה מסונכרנת (מנות מחרוזת) {בזמן (! העברה) {נסה {לחכות (); } לתפוס (InterruptedException e) {Thread.currentThread (). interrupt (); Log.error ("החוט הופרע", ה); }} העברה = שקר; this.packet = חבילה; notifyAll (); } מחרוזת ציבורית מסונכרנת לקבל () {בזמן (העברה) {נסה {לחכות (); } לתפוס (InterruptedException e) {Thread.currentThread (). interrupt (); Log.error ("החוט הופרע", ה); }} העברה = אמת; notifyAll (); חבילת החזרה; }}

בואו נשבר את מה שקורה כאן:

  • ה חֲבִילָה משתנה מציין את הנתונים המועברים דרך הרשת
  • יש לנו בוליאני מִשְׁתַנֶה העברה - ש ה שׁוֹלֵחַ ו מַקְלֵט ישמש לסינכרון:
    • אם משתנה זה הוא נָכוֹן, אז ה מַקְלֵט צריך לחכות ל שׁוֹלֵחַ כדי לשלוח את ההודעה
    • אם זה שֶׁקֶר, לאחר מכן שׁוֹלֵחַ צריך לחכות ל מַקְלֵט לקבל את ההודעה
  • ה שׁוֹלֵחַ שימושים לִשְׁלוֹחַ() שיטת שליחת נתונים אל מַקְלֵט:
    • אם לְהַעֲבִיר הוא שֶׁקֶר, נמתין בטלפון לַחֲכוֹת() על חוט זה
    • אבל כשזה כן נָכוֹן, אנו מחליפים את המצב, מגדירים את המסר שלנו ומתקשרים notifyAll () להעיר שרשורים אחרים כדי לציין שאירוע משמעותי התרחש והם יכולים לבדוק אם הם יכולים להמשיך בביצוע
  • באופן דומה, ה מַקְלֵט אשתמש לְקַבֵּל() שיטה:
    • אם ה לְהַעֲבִיר היה מוגדר ל שֶׁקֶר על ידי שׁוֹלֵחַואז רק זה ימשיך, אחרת נתקשר לַחֲכוֹת() על חוט זה
    • כאשר התקיים התנאי, אנו מחליפים את המצב, מודיעים לכל שרשורי ההמתנה להתעורר ומחזירים את חבילת הנתונים שהייתה מַקְלֵט

5.1. למה לצרף לַחֲכוֹת() ב בזמן לוּלָאָה?

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

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

5.2. מדוע אנו צריכים לסנכרן את sסוֹף() ו לְקַבֵּל() שיטות?

הצבנו את השיטות הללו בפנים מסונכרן שיטות לספק מנעולים מהותיים. אם חוט קורא לַחֲכוֹת() השיטה אינה הבעלים של הנעילה הטבועה, שגיאה תיזרק.

כעת ניצור שׁוֹלֵחַ ו מַקְלֵט וליישם את ניתן לרוץ ממשק על שניהם כך שהמופעים שלהם יוכלו להתבצע על ידי שרשור.

בואו נראה תחילה איך שׁוֹלֵחַ יעבוד:

מחלקה ציבורית משדר מיישם Runnable {data data data; // קונסטרוקטורים סטנדרטיים פועלים ללא ריק () {מחרוזת חבילות [] = {"חבילה ראשונה", "חבילה שנייה", "חבילה שלישית", "חבילה רביעית", "סוף"}; עבור (מנות מחרוזת: מנות) {data.send (מנות); // Thread.sleep () כדי לחקות עיבוד כבד בצד השרת נסה את {Thread.sleep (ThreadLocalRandom.current (). NextInt (1000, 5000)); } לתפוס (InterruptedException e) {Thread.currentThread (). interrupt (); Log.error ("החוט הופרע", ה); }}}}

לזה שׁוֹלֵחַ:

  • אנו יוצרים כמה מנות נתונים אקראיות שיישלחו לרשת פנימה חבילות [] מַעֲרָך
  • עבור כל מנה, אנחנו פשוט מתקשרים לִשְׁלוֹחַ()
  • ואז אנחנו מתקשרים Thread.sleep () עם מרווח אקראי לחיקוי עיבוד כבד בצד השרת

לבסוף, בואו ליישם את שלנו מַקְלֵט:

מקלט מחלקה ציבורית מיישם Runnable {עומס נתונים פרטי; // קונסטרוקציות סטנדרטיות של בונים סטנדרטיים () {עבור (String receivedMessage = load.receive ();! "End" .equals (receivedMessage); receivedMessage = load.receive ()) {System.out.println (receivedMessage); // ... נסה את {Thread.sleep (ThreadLocalRandom.current (). nextInt (1000, 5000)); } לתפוס (InterruptedException e) {Thread.currentThread (). interrupt (); Log.error ("החוט הופרע", ה); }}}}

הנה, אנחנו פשוט מתקשרים load.receive () בלופ עד שנקבל את האחרון "סוֹף" חבילת נתונים.

בואו נראה כעת את היישום הזה בפעולה:

ראשי סטטי ציבורי ריק (String [] args) {Data data = Data new (); שולח חוט = שרשור חדש (שולח חדש (נתונים)); מקלט חוטים = שרשור חדש (מקלט חדש (נתונים)); sender.start (); receiver.start (); }

נקבל את הפלט הבא:

חבילה ראשונה חבילה שנייה חבילה שלישית חבילה רביעית 

והנה אנחנו - קיבלנו את כל חבילות הנתונים בסדר הנכון, ברצף וקבע בהצלחה את התקשורת הנכונה בין השולח למקבל.

6. מסקנה

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

לפני שנסיים כאן, כדאי להזכיר כי כל ה- API ברמה נמוכה, כגון לַחֲכוֹת(), לְהוֹדִיעַ() ו notifyAll () - הן שיטות מסורתיות שעובדות טוב, אך לרוב מנגנונים ברמה גבוהה הם פשוטים וטובים יותר - כמו המקור של Java לנעול ו מַצָב ממשקים (זמין ב java.util.concurrent.locks חֲבִילָה).

למידע נוסף על java.util.concurrent חבילה, בקר בסקירה שלנו על המאמר java.util.concurrent, וכן לנעול ו מַצָב מכוסים במדריך java.util.concurrent.Locks, כאן.

כמו תמיד, קטעי הקוד המלאים המשמשים במאמר זה זמינים ב- GitHub.


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