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 מיישמת את זה:

  1. עבודה על עותק מלא (Snapshot / Copy-on-Write)
    למשל ב-CopyOnWriteArrayList:
    בכל פעם כשנרצה לשנות את האוסף (להוסיף או למחוק איבר), המערכת לא נוגעת במערך המקורי שכרגע קוראים ממנו.
    במקום זאת, היא מייצרת עותק חדש לחלוטין של כל המערך בזיכרון, עושה בו את השינוי, ורק אז מעבירה את ההפניה (Reference) כך שתצביע למערך החדש.
    מה קורה לאיטרטור? האיטרטור התחיל את הריצה שלו על העותק הישן (ה-Snapshot) שהיה קיים באותו רגע. מבחינתו, האוסף מעולם לא הששתנה! הוא עובר בשקט על העותק שלו עד הסוף, בלי לדעת ובלי אכפת לו שבינתיים העולם בחוץ השתנה.
  2. עבודה על מבנה נתונים מבודד / 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).
מצוינים וגבוהים מאוד
מאפשרים קריאה וכתיבה מקבילית ומבוזרת ללא צווארי בקבוק.
🏠 Back to Orly's Code Corner