Fail-Fast vs Fail-Safe Iterators
Fail-Fast Iterator ו Fail-Safe Iterator מתארים אסטרטגיות שונות שבהן נוקטים Collections ב-Java כאשר מבוצע שינוי במבנה הנתונים. למשל להוסיף או למחוק איבר, בזמן שעוברים עליו באמצעות איטרטור (Iterator).
Fail-Fast Iterator
ברירת המחדלי ברוב מבני הנתונים מסוג אוסף (collection) של JAVA כמו כמו ArrayList, HashMap, HashSet.
האסטרטגיה הזאת טובה למניעת התנהגות בלתי צפויה (Undefined Behavior), שינוי אוסף תוך כדי מעבר עליו עלול לגרום לקריאות זיכרון שגויות, ללולאות אינסופיות או לקריסת מערכת שקטה.
ב ArrayList רגיל, האיטרטור מביט ישירות על הזיכרון החי שבו האוסף נמצא. אם בזמן שהוא עובר על האיברים, Thread אחר מוסיף איבר או מוחק איבר, הזיכרון משתנה "תחת הרגליים" של האיטרטור. האיטרטור עלול לקרוא בטעות זיכרון ריק, לדלג על איברים, או להיכנס ללולאה אינסופית. לכן, כדי למנוע אסון, Java בחרה לזרוק שגיאה מיד ConcurrentModificationException.
איך זה עובד?
לאיטרטור יש משתנה פנימי שנקרא modCount , בכל פעם שאיבר נוסף או נמחק מהאוסף, המונה הזה גדל. כשהאיטרטור רץ (()next), הוא בודק בכל צעד האם המונה של האוסף השתנה מאז שהאיטרטור נוצר.
כדי למחוק איבר בצורה בטוחה עם Fail-Fast, חייבים להשתמש במתודה ()iterator.remove
אם האיטרטור מגלה שמישהו שינה את האוסף בזמן שהוא רץ, נזרקת שגיאה מסוג ConcurrentModificationException ומפסיק את הריצה.
Fail-Safe Iterator .
איטרטורים אלו מופיעים באוספים שמותאמים לתכנות מקבילי (Concurrent Collections), כמו ConcurrentHashMap או CopyOnWriteArrayList.
איך זה עובד?
האיטרטורים פועלים על עותק של האוסף המקורי, או על מנה נתונים מבודד ולא על האוסף ה"חי".
יש שתי שיטות עיקריות שבהן Java מיישמת את זה:
-
עבודה על עותק מלא (Snapshot / Copy-on-Write)
למשל ב-CopyOnWriteArrayList:
בכל פעם כשנרצה לשנות את האוסף (להוסיף או למחוק איבר), המערכת לא נוגעת במערך המקורי שכרגע קוראים ממנו.
במקום זאת, היא מייצרת עותק חדש לחלוטין של כל המערך בזיכרון, עושה בו את השינוי, ורק אז מעבירה את ההפניה (Reference) כך שתצביע למערך החדש.
מה קורה לאיטרטור? האיטרטור התחיל את הריצה שלו על העותק הישן (ה-Snapshot) שהיה קיים באותו רגע. מבחינתו, האוסף מעולם לא הששתנה! הוא עובר בשקט על העותק שלו עד הסוף, בלי לדעת ובלי אכפת לו שבינתיים העולם בחוץ השתנה. -
עבודה על מבנה נתונים מבודד / Weakly Consistent
למשל ב-ConcurrentHashMap
במפות מקביליות גדולות אי אפשר לשכפל את כל המפה בכל שינוי (זה היה זולל זיכרון ומאט את המערכת). לכן משתמשים באסטרטגיה של Weakly Consistent:
האיטרטור עובר על צמתים (Nodes) במבנה הנתונים, אבל המבנה בנוי כך שהוספה או מחיקה של איברים מתבצעת באמצעות שינוי מצביעים (Pointers) מבוקר וזהיר, שאינו שובר את מבנה העץ או הרשת שהאיטרטור מטייל עליה.
האיטרטור משקף את מצב האוסף לפחות מרגע שבו הוא נוצר, ואינו זורק שגיאה גם אם צמתים עודכנו תוך כדי.
לשם השוואה קלה והבנת ההבדלים נשווה בין HashMap לבין ConcurrentHashMap
| תכונה / מאפיין | HashMap | ConcurrentHashMap |
|---|---|---|
| Thread Safety |
Not Thread-Safe עלול לגרום לקריסות זיכרון או ללולאות אינסופיות בשימוש מקבילי. |
Thread-Safe מתוכנן במיוחד לעבודה בו-זמנית של מספר Threads. |
| מנגנון נעילה (Locking Mechanism) | ללא נעילות כלל (לא מותאם ל-Concurrency). | נעילות מתקדמות ברמת ה-Node/Bucket ושימוש ב-CAS (ללא נעילת כל המפה). |
| התנהגות האיטרטור (Iterator Type) | זורק שגיאת ConcurrentModificationException מיד אם האוסף משתנה בזמן ריצה. | Weakly Consistent (Fail-Safe מיוחד) עובד בשקט, אינו זורק שגיאה לעולם, אך עשוי להציג נתונים ישנים |
| תמיכה בערכי null / מפתחות NULL |
מותר מאפשר מפתח null אחד וערכי null כמה שרוצים. |
אסור לחלוטין זרקת NullPointerException בעת ניסיון להכניס מפתח או ערך null. |
| ביצועים בסביבת Single-Threaded |
מהירים יותר אין overhead של מנגנוני סנכרון ונעילות |
איטיים במעט דורשים ניהול סנכרון קל מאחורי הקלעים. |
| ביצועים בסביבת Multi-Threaded |
לא יציבים / מסוכנים דורשים סנכרון חיצוני ידני (כמו Collections.synchronizedMap). |
מצוינים וגבוהים מאוד מאפשרים קריאה וכתיבה מקבילית ומבוזרת ללא צווארי בקבוק. |