מדריך למילת המפתח הפכפכה בג'אווה

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

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

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

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

2. אדריכלות מרובת מעבדים משותפת

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

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

כאן נכנסת לתמונה היררכיית הזיכרון הבאה:

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

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

3. מתי להשתמש נָדִיף

כדי להרחיב יותר על קוהרנטיות המטמון, בואו נשאל דוגמה אחת מהספר Java Concurrency in Practice:

מחלקה ציבורית TaskRunner {מספר פרטי סטטי פרטי; מוכן בוליאני סטטי פרטי; מחלקה סטטית פרטית Reader מאריך את החוט {@Override public void run () {תוך (! מוכן) {Thread.yield (); } System.out.println (מספר); }} ראשי ריק סטטי ציבורי (String [] args) {Reader new (). start (); מספר = 42; מוכן = נכון; }}

ה TaskRunner class שומר על שני משתנים פשוטים. בשיטה העיקרית שלו, הוא יוצר חוט נוסף שמסתובב על ה- מוּכָן משתנה כל עוד זה שֶׁקֶר. כאשר המשתנה הופך נָכוֹן, החוט פשוט ידפיס את מספר מִשְׁתַנֶה.

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

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

3.1. נראות זיכרון

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

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

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

עם כל האמור, כאשר החוט הראשי מעדכן את מספר ו מוּכָן משתנים, אין שום ערובה לגבי מה שרשור הקורא עשוי לראות. במילים אחרות, חוט הקורא עשוי לראות את הערך המעודכן מיד, או באיחור כלשהו, ​​או לעולם לא!

נראות זיכרון זו עלולה לגרום לבעיות חי בתוכניות הנשענות על נראות.

3.2. סידור מחדש

להחמיר את העניינים, חוט הקורא עשוי לראות את הכותבים בכל סדר אחר שאינו סדר התוכנית בפועל. לדוגמא, מאז שאנחנו מעדכנים את ה- מספר מִשְׁתַנֶה:

סטטי ציבורי ריק ריק (String [] args) {Reader new (). start (); מספר = 42; מוכן = נכון; }

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

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

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

3.3. נָדִיף סדר זיכרון

כדי להבטיח שעדכונים למשתנים יתפשטו באופן צפוי לשרשורים אחרים, עלינו להחיל את נָדִיף משנה למשתנים אלה:

TaskRunner בכיתה ציבורית {מספר פרטי סטטי נדיף פרטי; פרטי בוליאני סטטי נדיף פרטי; // כמו מקודם }

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

4. נָדִיף וסינכרון חוטים

עבור יישומים מרובי-הברגה, עלינו להבטיח כמה כללים להתנהגות עקבית:

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

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

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

5. קורה-לפני ההזמנה

השפעות נראות הזיכרון של נָדִיף משתנים חורגים מעבר ל נָדִיף משתנים עצמם.

כדי להפוך את העניין לקונקרטי יותר, נניח שחוט A כותב ל- a נָדִיף משתנה, ואז חוט B קורא אותו דבר נָדִיף מִשְׁתַנֶה. במקרים כאלו, הערכים שהיו גלויים ל- A לפני כתיבת ה- נָדִיף המשתנה יהיה גלוי ל- B לאחר קריאת ה- נָדִיף מִשְׁתַנֶה:

מבחינה טכנית, כל כתיבה לא נָדִיף שדה מתרחש לפני כל קריאה שלאחר מכן של אותו שדה. זה נָדִיף כלל משתנה של מודל זיכרון Java (JMM).

5.1. חזיר חזיר

בגלל עוצמת הקורה לפני הזמנת הזיכרון, לפעמים אנו יכולים לחזור על תכונות הראות של אחר נָדִיף מִשְׁתַנֶה. לדוגמה, בדוגמה המסוימת שלנו, אנחנו רק צריכים לסמן את מוּכָן משתנה כ נָדִיף:

מחלקה ציבורית TaskRunner {מספר פרטי סטטי פרטי; // לא נדיף פרטי בוליאני סטטי נדיף מוכן; // כמו מקודם }

כל דבר לפני הכתיבה נָכוֹן אל ה מוּכָן משתנה גלוי לכל דבר לאחר קריאת ה- מוּכָן מִשְׁתַנֶה. לכן, ה מספר פיג'יקים משתנים על נראות הזיכרון שנאכפת על ידי מוּכָן מִשְׁתַנֶה. במילים פשוטות, למרות שזה לא א נָדִיף משתנה, הוא מציג א נָדִיף התנהגות.

על ידי שימוש בסמנטיקה זו, אנו יכולים להגדיר רק כמה מהמשתנים בכיתה שלנו כ- נָדִיף ולייעל את ערבות הנראות.

6. מסקנה

במדריך זה חקרנו עוד אודות נָדִיף מילת המפתח ויכולותיה, כמו גם השיפורים שנעשו בה החל מ- Java 5.

כמו תמיד, ניתן למצוא את דוגמאות הקוד ב- GitHub.