java.util.ConcurrentModificationException

java.util.ConcurrentModificationException היא חריגה נפוצה מאוד כאשר עובדים עם מחלקות אוסף ב-Java. מחלקות אוסף ב-Java הן נכשלות-מהר, שזה אומר שאם האוסף ישתנה בזמן שחוטפים עליו תהליכים בעזרת iterator, ה־iterator.next() יזרוק ConcurrentModificationException. חריגת שינוי משווה יכולה להתרחש במקרה של סביבת תכנות ג'אווה מרובה-לסרט וגם סביבה תכנותית בודדת.

java.util.ConcurrentModificationException

בואו נראה את תרחיש חריגת שינוי משווה עם דוגמה.

package com.journaldev.ConcurrentModificationException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class ConcurrentModificationExceptionExample {

	public static void main(String args[]) {
		List myList = new ArrayList();

		myList.add("1");
		myList.add("2");
		myList.add("3");
		myList.add("4");
		myList.add("5");

		Iterator it = myList.iterator();
		while (it.hasNext()) {
			String value = it.next();
			System.out.println("List Value:" + value);
			if (value.equals("3"))
				myList.remove(value);
		}

		Map myMap = new HashMap();
		myMap.put("1", "1");
		myMap.put("2", "2");
		myMap.put("3", "3");

		Iterator it1 = myMap.keySet().iterator();
		while (it1.hasNext()) {
			String key = it1.next();
			System.out.println("Map Value:" + myMap.get(key));
			if (key.equals("2")) {
				myMap.put("1", "4");
				// myMap.put("4", "4");
			}
		}

	}
}

התוכנית לעיל תזרוק java.util.ConcurrentModificationException בעת ביצוע, כפי שמוצג בלוגי הקונסולה למטה.

List Value:1
List Value:2
List Value:3
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:937)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:891)
	at com.journaldev.ConcurrentModificationException.ConcurrentModificationExceptionExample.main(ConcurrentModificationExceptionExample.java:22)

הפלט של עקבות השגיאה מציין בבירור שהחריגה ממוצעת כאשר אנו קוראים לפונקציית next() של ה-iterator. אם אתה שואל איך ה-Iterator בודק לשינוי, המימוש שלו נמצא במחלקת AbstractList, שם מוגדרת משתנה שלם בשם modCount. ערך ה-modCount מספק את מספר הפעמים שגודל הרשימה השתנה. ערך ה-modCount נעשה שימוש בכל קריאה ל-next() כדי לבדוק שינויים בפונקציה checkForComodification(). כעת, הוסף הערות לחלק הרשימה והפעל את התוכנית שוב. תראה שלא יתרחש שם חריגת ConcurrentModificationException. פלט:

Map Value:3
Map Value:2
Map Value:4

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

כדי למנוע את ConcurrentModificationException בסביבה מרובה-תהליכים

  1. ניתן להמיר את הרשימה למערך ולאחר מכן לעבור עליו באמצעות iterator. שיטה זו עובדת טוב עבור רשימה קטנה או בינונית, אך אם הרשימה גדולה, יש לזכור שזה ישפיע על ביצועים.
  2. אתה יכול לנעול את הרשימה במהלך ההיטלפות על ידי הנחתה בבלוק מסונכרן. הגישה הזו אינה מומלצת מאחר והיא תפסיק את היתרונות של רב-תהליכיות.
  3. אם אתה משתמש ב-JDK1.5 או גרסה גבוהה יותר, אתה יכול להשתמש במחלקות ConcurrentHashMap ו-CopyOnWriteArrayList. זו הגישה המומלצת כדי למנוע חריגת שינוי סמוכי.

כדי למנוע את ConcurrentModificationException בסביבת תהליך יחיד

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

package com.journaldev.ConcurrentModificationException;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

public class AvoidConcurrentModificationException {

	public static void main(String[] args) {

		List<String> myList = new CopyOnWriteArrayList<String>();

		myList.add("1");
		myList.add("2");
		myList.add("3");
		myList.add("4");
		myList.add("5");

		Iterator<String> it = myList.iterator();
		while (it.hasNext()) {
			String value = it.next();
			System.out.println("List Value:" + value);
			if (value.equals("3")) {
				myList.remove("4");
				myList.add("6");
				myList.add("7");
			}
		}
		System.out.println("List Size:" + myList.size());

		Map<String, String> myMap = new ConcurrentHashMap<String, String>();
		myMap.put("1", "1");
		myMap.put("2", "2");
		myMap.put("3", "3");

		Iterator<String> it1 = myMap.keySet().iterator();
		while (it1.hasNext()) {
			String key = it1.next();
			System.out.println("Map Value:" + myMap.get(key));
			if (key.equals("1")) {
				myMap.remove("3");
				myMap.put("4", "4");
				myMap.put("5", "5");
			}
		}

		System.out.println("Map Size:" + myMap.size());
	}

}

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

List Value:1
List Value:2
List Value:3
List Value:4
List Value:5
List Size:6
Map Value:1
Map Value:2
Map Value:4
Map Value:5
Map Size:4

מהדוגמה לעיל ניתן לראות ש:

  1. כיתות אוסף קונקרנטיות ניתנות לשינוי בצורה בטוחה, הן לא תזרוק חריגת ConcurrentModificationException.

  2. במקרה של CopyOnWriteArrayList, ה-iterator לא מתאים את השינויים ברשימה ופועל על הרשימה המקורית.

  3. במקרה של ConcurrentHashMap, ההתנהגות אינה תמיד זהה. תחת התנאי:

    אם (key.equals("1")){
    	myMap.remove("3");}
    

    הפלט הוא:

    ערך המפה:1
    ערך המפה:כלום
    ערך המפה:4
    ערך המפה:2
    גודל המפה:4
    

    הוא מקבל את האובייקט החדש שהוסיף עם המפתח "4" אך לא את האובייקט הבא שהוסיף עם המפתח "5". עכשיו אם אני משנה את התנאי לכך.

    אם (key.equals("3")){
    	myMap.remove("2");}
    

    הפלט הוא:

    ערך המפה:1
    ערך המפה:3
    ערך המפה:כלום
    גודל המפה:4
    

    במקרה זה, הוא לא מתחשב באובייקטים שהוספו לאחרונה. אז אם אתה משתמש ב-ConcurrentHashMap אז תמנע מהוספת אובייקטים חדשים כיוון שניתן לעבד אותם תלוי ב-keyset. שים לב שאותה תוכנית יכולה להדפיס ערכים שונים במערכת שלך מכיוון שה-keyset של HashMap אינו מסודר.

השתמש בלולאת for כדי למנוע את java.util.ConcurrentModificationException

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

for(int i = 0; i<myList.size(); i++){
	System.out.println(myList.get(i));
	if(myList.get(i).equals("3")){
		myList.remove(i);
		i--;
		myList.add("6");
	}
}

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

package com.journaldev.ConcurrentModificationException;

import java.util.ArrayList;
import java.util.List;

public class ConcurrentModificationExceptionWithArrayListSubList {

	public static void main(String[] args) {

		List names = new ArrayList<>();
		names.add("Java");
		names.add("PHP");
		names.add("SQL");
		names.add("Angular 2");

		List first2Names = names.subList(0, 2);

		System.out.println(names + " , " + first2Names);

		names.set(1, "JavaScript");
		// בדוק את הפלט למטה. :)
		System.out.println(names + " , " + first2Names);

		// בוא נשנה את גודל הרשימה ונקבל ConcurrentModificationException
		names.add("NodeJS");
		System.out.println(names + " , " + first2Names); // this line throws exception

	}

}

פלט התוכנית לעיל הוא:

[Java, PHP, SQL, Angular 2] , [Java, PHP]
[Java, JavaScript, SQL, Angular 2] , [Java, JavaScript]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1282)
	at java.base/java.util.ArrayList$SubList.listIterator(ArrayList.java:1151)
	at java.base/java.util.AbstractList.listIterator(AbstractList.java:311)
	at java.base/java.util.ArrayList$SubList.iterator(ArrayList.java:1147)
	at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:465)
	at java.base/java.lang.String.valueOf(String.java:2801)
	at java.base/java.lang.StringBuilder.append(StringBuilder.java:135)
	at com.journaldev.ConcurrentModificationException.ConcurrentModificationExceptionWithArrayListSubList.main(ConcurrentModificationExceptionWithArrayListSubList.java:26)

לפי תיעוד ה־ArrayList של subList, שינויים מבניים מותרים רק על הרשימה שמוחזרת על ידי השיטה subList. כל השיטות ברשימה שמוחזרת בודקות תחילה האם ה־modCount האמיתי של הרשימה המקורית שווה לערך הצפוי שלה ומזרים חריגת ConcurrentModificationException אם היא לא.

אתה יכול להוריד את כל קוד הדוגמא מה־מאגר ה-GitHub שלנו.

Source:
https://www.digitalocean.com/community/tutorials/java-util-concurrentmodificationexception