מבוא למשתנים אטומיים בג'אווה

1. הקדמה

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

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

2. מנעולים

בואו נסתכל על הכיתה:

מונה ציבורי מונה {מונה int; תוספת חלל ציבורית () {counter ++; }}

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

זאת בגלל פעולת התוספת הגדולה (מונה ++), שאולי נראה כמו פעולה אטומית, אך למעשה הוא שילוב של שלוש פעולות: השגת הערך, תוספת וכתיבת הערך המעודכן בחזרה.

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

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

מעמד ציבורי SafeCounterWithLock {דלפק פרטי נדיף פרטי; תוספת חלל מסונכרנת ציבורית () {counter ++; }}

בנוסף, עלינו להוסיף את ה- נָדִיף מילת מפתח כדי להבטיח נראות עיון נכונה בין האשכולות.

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

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

תהליך ההשעיה ואז חידוש החוט הוא יקר מאוד ומשפיע על היעילות הכוללת של המערכת.

בתוכנית קטנה, כמו דֶלְפֵּק, הזמן המושקע בהחלפת הקשר עשוי להפוך להרבה יותר מביצוע קוד בפועל, ובכך להפחית מאוד את היעילות הכוללת.

3. פעולות אטומיות

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

פעולת CAS טיפוסית פועלת בשלושה אופרנדים:

  1. מיקום הזיכרון בו פועל (M)
  2. הערך הצפוי הקיים (A) של המשתנה
  3. הערך החדש (B) שצריך להגדיר

פעולת CAS מעדכנת באופן אטומי את הערך M עד B, אך רק אם הערך הקיים ב- M תואם A, אחרת לא ננקטת פעולה.

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

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

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

4. משתנים אטומיים בג'אווה

הכיתות המשתנות האטומיות הנפוצות ביותר ב- Java הן AtomicInteger, AtomicLong, AtomicBoolean ו- AtomicReference. שיעורים אלה מייצגים int, ארוך, בוליאני, והתייחסות אובייקט בהתאמה אשר ניתן לעדכן אטומית. השיטות העיקריות שנחשפו בשיעורים אלה הן:

  • לקבל() - מקבל את הערך מהזיכרון, כך שניתן יהיה לראות שינויים שנעשו על ידי שרשורים אחרים; שווה ערך לקריאה א נָדִיף מִשְׁתַנֶה
  • מַעֲרֶכֶת() - כותב את הערך לזיכרון, כך שהשינוי גלוי לשרשורים אחרים; שווה ערך לכתיבה א נָדִיף מִשְׁתַנֶה
  • lazySet () - בסופו של דבר כותב את הערך לזיכרון, אולי מסודר מחדש עם פעולות זיכרון רלוונטיות לאחר מכן. מקרה שימוש אחד הוא ביטול הפניות, לצורך איסוף האשפה, שלעולם לא ניתן יהיה לגשת אליו יותר. במקרה זה, ביצועים טובים יותר מושגים על ידי עיכוב האפס נָדִיף לִכתוֹב
  • CompareAndSet () - זהה כמתואר בסעיף 3, מחזיר נכון כשהוא מצליח, אחרת שקר
  • weakCompareAndSet () - זהה כמתואר בסעיף 3, אך חלש יותר במובן זה שהוא אינו יוצר הזמנות לפני. פירוש הדבר שהוא לא יכול בהכרח לראות עדכונים שבוצעו למשתנים אחרים. נכון ל- Java 9, שיטה זו הוצאה משימוש בכל היישומים האטומיים לטובת weakCompareAndSetPlain (). השפעות הזיכרון של weakCompareAndSet () היו פשוטים אך בשמותיהם נרמזו השפעות זיכרון נדיפות. כדי למנוע בלבול זה, הם ביטלו שיטה זו והוסיפו ארבע שיטות עם אפקטים שונים של זיכרון כגון weakCompareAndSetPlain () אוֹ חלשההשוואה וסט סטוליט ()

דלפק בטוח לחוטים המיושם עם AtomicInteger מוצג בדוגמה שלהלן:

מחלקה ציבורית SafeCounterWithoutLock {מונה פרטי AtomicInteger סופי = AtomicInteger חדש (0); public int getValue () {return counter.get (); } תוספת חלל ציבורית () {while (true) {int existingValue = getValue (); int newValue = eksisterende ערך + 1; אם (counter.compareAndSet (existingValue, newValue)) {return; }}}}

כפי שאתה יכול לראות, אנו מנסים שוב את CompareAndSet פעולה ושוב על כישלון, מכיוון שאנו רוצים להבטיח כי השיחה אל תוֹסֶפֶת השיטה תמיד מגדילה את הערך ב- 1.

5. מסקנה

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

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

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