java.util.ConcurrentModificationException هي استثناء شائع جدًا عند العمل مع فئات مجموعات جافا. فئات مجموعات جافا هي فئات fail-fast، والتي تعني أنه إذا تم تغيير المجموعة أثناء تنقل خيط ما فوقها باستخدام محدد، سيُلقي 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()` من المحدد. إذا كنت تتساءل عن كيفية فحص المحدد للتعديل، فإن تنفيذه موجود في فئة abstractlist، حيث يتم تعريف متغير صحيح `modcount`. يوفر modcount عدد المرات التي تم فيها تغيير حجم القائمة. يُستخدم قيمة modcount في كل استدعاء `next()` للتحقق من وجود أي تعديلات في وظيفة `checkforcomodification()`. الآن، قم بتعليق جزء القائمة وقم بتشغيل البرنامج مرة أخرى. سترى أنه لا يتم رمي أي استثناء concurrentmodificationexception الآن. الإخراج:
Map Value:3
Map Value:2
Map Value:4
نظرًا لأننا نقوم بتحديث قيمة المفتاح الحالي في mymap، فإن حجمها لم يتغير ولم نحصل على concurrentmodificationexception. قد تكون النتيجة مختلفة في نظامك لأن keyset الخاص بـ hashmap غير مرتب كقائمة. إذا قمت بإلغاء تعليق البيان حيث أقوم بإضافة مفتاح وقيمة جديدين في hashmap، سيتسبب ذلك في استثناء concurrentmodificationexception.
لتجنب استثناء concurrentmodificationexception في بيئة متعددة الخيوط
- يمكنك تحويل القائمة إلى مصفوفة ثم التكرار على المصفوفة. يعمل هذا النهج بشكل جيد للقوائم الصغيرة أو المتوسطة الحجم ولكن إذا كانت القائمة كبيرة، فسيؤثر ذلك على الأداء كثيرًا.
- يمكنك قفل القائمة أثناء التكرار عن طريق وضعها في كتلة متزامنة. هذا النهج غير مستحسن لأنه سيتوقف عن الاستفادة من التعددية.
- إذا كنت تستخدم JDK1.5 أو أحدث ، يمكنك استخدام فئات ConcurrentHashMap و CopyOnWriteArrayList. هذا هو النهج الموصى به لتجنب استثناء التعديل المتزامن.
لتجنب استثناء ConcurrentModificationException في بيئة ذات خيط واحد
يمكنك استخدام دالة remove()
للمحدث لإزالة الكائن من الكائن المجموعة الأساسية. ولكن في هذه الحالة ، يمكنك إزالة نفس الكائن وليس أي كائن آخر من القائمة. دعونا نشغل مثالًا باستخدام فئات المجموعة المتزامنة.
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
من المثال أعلاه واضح أن:
-
يمكن تعديل فئات المجموعة المتزامنة بأمان ، لن يتم رمي استثناء ConcurrentModificationException.
-
في حالة CopyOnWriteArrayList ، لا يأخذ المكرر في اعتباره التغييرات في القائمة ويعمل على القائمة الأصلية.
-
في حالة ConcurrentHashMap، السلوك ليس دائمًا هو نفسه. بالنسبة للشرط:
if(key.equals("1")){ myMap.remove("3");}
الناتج هو:
Map Value:1 Map Value:null Map Value:4 Map Value:2 Map Size:4
يتم أخذ الكائن الجديد الذي تمت إضافته بمفتاح “4” ولكن لا يتم أخذ الكائن الذي تمت إضافته بمفتاح “5”. الآن إذا قمت بتغيير الشرط إلى الآتي.
if(key.equals("3")){ myMap.remove("2");}
الناتج هو:
Map Value:1 Map Value:3 Map Value:null Map Size:4
في هذه الحالة، لا يأخذ في اعتباره الكائنات الجديدة المضافة. لذلك إذا كنت تستخدم ConcurrentHashMap، تجنب إضافة كائنات جديدة حيث يمكن معالجتها اعتمادًا على مجموعة المفاتيح. يرجى ملاحظة أن نفس البرنامج قد يطبع قيمًا مختلفة في نظامك لأن HashMap keyset ليست مرتبة.
استخدم حلقة 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