Java 101: הבנת שרשורי Java, חלק 3: תזמון חוטים והמתנה / הודעה

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

שים לב שמאמר זה (חלק מארכיוני JavaWorld) עודכן עם רשימות קוד חדשות וקוד מקור להורדה במאי 2013.

הבנת נושאי Java - קרא את כל הסדרה

  • חלק 1: הצגת חוטים ורצים
  • חלק 2: סנכרון
  • חלק 3: תזמון חוטים, המתנה / הודעה והפרעה לשרשור
  • חלק 4: קבוצות חוטים, תנודתיות, משתנים מקומיים של חוטים, טיימרים ומוות חוטים

תזמון חוטים

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

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

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

  1. ג'אווה לא מכריח VM לתזמן שרשורים באופן ספציפי או להכיל מתזמן שרשור. זה מרמז על תזמון חוטים תלוי פלטפורמה. לכן, עליך לנקוט משנה זהירות בעת כתיבת תוכנית Java אשר התנהגותה תלויה באופן מתוזמן של נושאים ועליה לפעול באופן עקבי בפלטפורמות שונות.
  2. למרבה המזל, בעת כתיבת תוכניות ג'אווה, עליך לחשוב כיצד ג'אווה מתזמנת שרשורים רק כאשר לפחות אחד משרשורי התוכנית שלך משתמש בכבדות במעבד במשך תקופות זמן ארוכות ותוצאות ביניים של ביצוע השרשור הזה מוכיחות. לדוגמא, יישומון מכיל שרשור שיוצר תמונה באופן דינמי. מעת לעת אתה רוצה שחוט הציור ישרטט את התוכן הנוכחי של התמונה כדי שהמשתמש יוכל לראות כיצד התמונה מתקדמת. כדי להבטיח כי חוט החישוב אינו מונופול המעבד, שקול תזמון חוטים.

בחן תוכנית שיוצרת שני שרשורים עתירי מעבד:

רישום 1. SchedDemo.java

// SchedDemo.java class SchedDemo { public static void main (String [] args) { new CalcThread ("CalcThread A").start (); new CalcThread ("CalcThread B").start (); } } class CalcThread extends Thread { CalcThread (String name) { // Pass name to Thread layer. super (name); } double calcPI () { boolean negative = true; double pi = 0.0; for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; return pi; } public void run () { for (int i = 0; i < 5; i++) System.out.println (getName () + ": " + calcPI ()); } }

SchedDemoיוצר שני שרשורים שכל אחד מהם מחשב את הערך של pi (חמש פעמים) ומדפיס כל תוצאה. תלוי איך יישום ה- JVM שלך מתזמן אשכולות, ייתכן שתראה פלט הדומה להודעות הבאות:

CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

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

CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

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

  1. מצב התחלתי: תוכנית יצרה את אובייקט החוט של השרשור, אך השרשור עדיין לא קיים מכיוון start()שעדיין לא נקראה לשיטת אובייקט השרשור .
  2. מצב רץ: זהו מצב ברירת המחדל של השרשור. לאחר סיום השיחה, ניתן להפעיל start()חוט בין אם חוט זה פועל ובין אם לא, כלומר באמצעות המעבד. אף על פי שאפשר להריץ שרשורים רבים, רק אחד פועל כרגע. מתזמני הברגה קובעים איזה שרשור ניתן להפעיל למעבד.
  3. מצב חסימה: כאשר חוט מבצע את sleep(), wait()או join()שיטות, כאשר ניסיונות חוט לקרוא נתונים אינם זמינים עדיין מרשת, וכאשר מחכה חוט לרכוש מנעול, החוט נמצא במצב חסום: זה לא פועל ולא במצב לרוץ. (אתה בטח יכול לחשוב על זמנים אחרים שבהם חוט ימתין שמשהו יקרה.) כאשר חוט חסום מבטל את החסימה, חוט זה עובר למצב ההפעלה.
  4. סיום מצב: לאחר הביצוע משאיר את run()שיטת השרשור, שרשור זה נמצא במצב סיום. במילים אחרות, החוט מפסיק להתקיים.

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

תזמון חוט ירוק

לא כל מערכות ההפעלה, מערכת החלונות הקדומה של Microsoft Windows 3.1, למשל, תומכות בשרשור. עבור מערכות כאלה, Sun Microsystems יכולה לתכנן JVM המחלק את חוט הביצוע היחיד שלה למספר נושאים. ה- JVM (לא מערכת ההפעלה הבסיסית) מספק את לוגיקת ההשחלה ומכיל את מתזמן השרשור. שרשורי JVM הם שרשורים ירוקים, או שרשורי משתמש .

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

הערה: לא תמיד יפעל ריצת רץ עם עדיפות גבוהה ביותר. הנה עדיפות למפרט שפת Java :

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

הודאה זו אומרת הרבה על יישום JVM של חוט ירוק. אותם מכשירי JVM אינם יכולים להרשות לעצמם לתת לחוטים לחסום כי זה יקשור את חוט הביצוע היחיד של ה- JVM. לכן, כאשר חוט חייב להיחסם, כגון כאשר חוט זה קורא נתונים לאט להגיע מקובץ, ה- JVM עשוי לעצור את ביצוע השרשור ולהשתמש במנגנון סקרים כדי לקבוע מתי מגיעים נתונים. בעוד שהשרשור נשאר עצור, מתזמן החוטים של JVM עשוי לתזמן שרשור עדיפות להפעלה. נניח שנתונים מגיעים בזמן שרשור העדיפות הנמוכה פועל. אף על פי שהשרשור בעל העדיפות הגבוהה יותר אמור לפעול ברגע שהנתונים מגיעים, זה לא יקרה עד שה- JVM יסקר אחר כך את מערכת ההפעלה ויגלה את ההגעה. לפיכך, הברגה בעדיפות נמוכה יותר פועלת למרות שחוט העדיפות הגבוהה יותר אמור לפעול.עליכם לדאוג למצב זה רק כאשר אתם זקוקים להתנהגות בזמן אמת מג'אווה. אבל אז ג'אווה היא לא מערכת הפעלה בזמן אמת, אז למה לדאוג?

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

בזמן T0, החוט הראשי מתחיל לפעול. בזמן T1, החוט הראשי מתחיל את חוט החישוב. מכיוון שלשרשור החישוב עדיפות נמוכה יותר מהשרשור הראשי, שרשור החישוב ממתין למעבד. בזמן T2, החוט הראשי מתחיל את חוט הקריאה. מכיוון שלחוט הקריאה יש עדיפות גבוהה יותר מהשרשור הראשי, החוט הראשי ממתין למעבד בזמן שחוט הקריאה פועל. בזמן T3 חוט הקריאה נחסם והחוט הראשי פועל. בזמן T4, חוט הקריאה נפתח ונפתח; החוט הראשי ממתין. לבסוף, בזמן T5, חוט הקריאה נחסם והחוט הראשי פועל. החלפה זו בביצוע בין הקריאה לשרשור הראשי נמשכת כל עוד התוכנית פועלת. חוט החישוב לעולם אינו פועל מכיוון שיש לו את העדיפות הנמוכה ביותר ובכך גווע מתשומת לב המעבד,מצב המכונהמעבד רעב .

אנו יכולים לשנות תרחיש זה על ידי מתן חוט החישוב עדיפות זהה לחוט הראשי. איור 2 מציג את התוצאה, החל מהזמן T2. (לפני T2, איור 2 זהה לאיור 1.)

בזמן T2, חוט הקריאה פועל בעוד שרשורי הראשי והחישוב מחכים למעבד. בזמן T3 חוט הקריאה נחסם ושרשור החישוב פועל מכיוון שהחוט הראשי רץ ממש לפני חוט הקריאה. בזמן T4, חוט הקריאה נפתח ונפתח; החוטים העיקריים והחישובים ממתינים. בזמן T5 חוט הקריאה נחסם והחוט הראשי פועל מכיוון שחוט החישוב רץ ממש לפני חוט הקריאה. החלפה זו בביצוע בין שרשור הראשי לחישוב נמשכת כל עוד התוכנית פועלת ותלויה בשרשור החסימה הפועל וחסימתו.

עלינו לשקול פריט אחרון בתזמון החוטים הירוקים. מה קורה כאשר חוט בעדיפות נמוכה יותר מחזיק מנעול שדורש עדיף גבוה יותר? החוטים בעלי העדיפות הגבוהה יותר חוסמים מכיוון שהוא לא יכול להשיג את הנעילה, מה שמרמז שלחוט העדיפות הגבוהה יותר יש למעשה אותה עדיפות כמו לשרשור העדיפות הנמוכה יותר. לדוגמא, חוט עדיפות 6 מנסה לרכוש מנעול אשר נושא עדיפות 3 מחזיק. מכיוון שחוט העדיפות 6 חייב להמתין עד שיוכל לרכוש את הנעילה, לשרשור העדיפות 6 יש עדיפות 3 - תופעה המכונה היפוך עדיפות .

היפוך עדיפות יכול לעכב מאוד את ביצוע הפתיל בעדיפות גבוהה יותר. לדוגמא, נניח שיש לך שלושה שרשורים עם סדר עדיפויות של 3, 4, ו- 9. שרשור 3 של עדיפות פועל והשאר שרשורים חסומים. נניח כי חוט העדיפות 3 תופס נעילה, ועדיפות 4 החוטים מבטלת את החסימה. שרשור העדיפות 4 הופך לשרשור הפועל כרגע. מכיוון שחוט העדיפות 9 מחייב את הנעילה, הוא ממשיך להמתין עד לשרשור העדיפות 3 לשחרר את הנעילה. עם זאת, חוט העדיפות 3 אינו יכול לשחרר את הנעילה עד שעדיפות 4 הברגה חוסמת או מסתיימת. כתוצאה מכך, חוט העדיפות 9 מעכב את ביצועו.