1. مقدمة في الفشل بسرعة
ويسمى "Fast Fail" أيضًا فشلًا ، وهي آلية للكشف عن الأخطاء لمجموعات Java. عندما يتكرر مؤشر ترابط على المجموعة ، لا يُسمح لخيوط أخرى بتعديل المجموعة من الناحية الهيكلية.
على سبيل المثال: افترض أن هناك خيطان (الموضوع 1 والموضوع 2) ، ومرور الخيط 1 يعبر العناصر في تعيين A عبر ITerator. في مرحلة ما ، يعدل الموضوع 2 بنية SET A (تعديل على الهيكل ، بدلاً من تعديل محتوى العنصر المحدد) ، فإن البرنامج سوف يرمي استثناءً من ذلك ، مما يولد فشلًا.
لا يمكن ضمان سلوك الفشل السريع للمؤلف ، ولا يمكن أن يضمن حدوث الخطأ ، لذلك يجب استخدام ConversIdificationException فقط للكشف عن الأخطاء.
تفشل جميع فئات التجميع في حزمة Java.Util بسرعة ، بينما تفشل فئات التجميع في حزمة Java.Util.Concurrent بأمان ؛
إن التكرار الذي يفشل بسرعة يلقي بتوافق ConcurrentModificationException ، في حين أن التكرار الذي يفشل بأمان لا يلقي هذا الاستثناء أبدًا.
2 أمثلة فاشلة
نموذج الرمز: (FastFailtest.java)
استيراد java.util.* ؛ استيراد java.util.concurrent.* ؛/** desc برنامج اختبار Fast-Fail في مجموعة Java. * * شروط الحدث السريع: عندما تعمل مؤشرات ترابط متعددة على المجموعة ، إذا كان أحد مؤشرات الترابط يعبر المجموعة من خلال التكرار ، يتم تغيير محتوى المجموعة بواسطة مؤشرات الترابط الأخرى ؛ سيتم طرح استثناء ConcurrentModificationException. * حل Fast-Fail: إذا قمت بمعالجته من خلال الفئة المقابلة ضمن حزمة مجموعة util.concurrent ، فلن يتم إنشاء حدث Fast Fail. * * في هذا المثال ، يتم اختبار حالتين من ArrayList و CopyOnWriteArrayList على التوالي. ستقوم ArrayList بإنشاء حدث سريع ، في حين أن CopyOnWriteArrayList لن يولد حدثًا سريعًا. * (01) عند استخدام ArrayList ، سيتم إنشاء حدث Fast-Tail وسيتم إلقاء استثناء ConcurrentModificationException ؛ التعريف كما يلي: * قائمة ثابتة خاصة <String> قائمة = ArrayList New ArrayList <String> () ؛ * (02) عند استخدام CopyOnWriteArrayList ، لن يتم إنشاء حدث سريع ؛ التعريف كما يلي: * قائمة ثابتة خاصة <String> قائمة = New CopyOnWriteArrayList <String> () ؛ * * Author Skywang */public class fastFaiLtest {قائمة ثابتة خاصة <string> list = new ArrayList <String> () ؛ // قائمة ثابتة خاصة <string> list = new CopyOnWriteArrayList <string> () ؛ الفراغ الثابت العام (سلسلة [] args) {// ابدأ موضوعين في نفس الوقت للعمل في القائمة! new ThreadOne (). start () ؛ new threadtwo (). start () ؛ } private static void printall () {system.out.println ("") ؛ قيمة السلسلة = فارغة ؛ iterator iter = list.iterator () ؛ بينما (iter.hasnext ()) {value = (string) iter.next () ؛ system.out.print (value+"،") ؛ }} /*** أضف 0،1،2،3،4،5 إلى القائمة بدورها. بعد إضافة رقم ، يتكرر من خلال printall () */ private static class threadone يمتد مؤشر الترابط {public void run () {int i = 0 ؛ بينما (i <6) {list.add (string.valueof (i)) ؛ printall () ؛ i ++ ؛ }}} /*** أضف 10،11،12،13،14،15 إلى القائمة بدورها. بعد إضافة كل رقم ، سوف يعبر القائمة بأكملها من خلال printall () */ private static class threadtwo يمتد مؤشر الترابط {public void run () {int i = 10 ؛ بينما (i <16) {list.add (string.valueof (i)) ؛ printall () ؛ i ++ ؛ }}}} قم بتشغيل الكود كنتيجة لذلك ويرمي استثناءً java.util.concurrentModificationException! وهذا هو ، يتم إنشاء حدث فاشل!
وصف النتائج
(01) في FastFailTest ، ابدأ اثنين من موضوعين في نفس الوقت لتشغيل القائمة من خلال ThreadOne (). start () و threadtwo (). start ().
ThreadOne Thread: إضافة 0 ، 1 ، 2 ، 3 ، 4 ، 5 إلى القائمة. بعد إضافة كل رقم ، يتم اجتياز القائمة بأكملها من خلال printall ().
Threadtwo Thread: أضف 10 ، 11 ، 12 ، 13 ، 14 ، 15 إلى القائمة. بعد إضافة كل رقم ، يتم اجتياز القائمة بأكملها من خلال printall ().
(02) عندما يعبر مؤشر ترابط القائمة ، يتم تغيير محتوى القائمة بواسطة مؤشر ترابط آخر ؛ سيتم إلقاء استثناء من ConcurrentModificationException ، مما يؤدي إلى حدث فاشل.
3. حل فشل سريع
آلية الفشل هي آلية الكشف عن الأخطاء. لا يمكن استخدامه إلا للكشف عن الأخطاء ، لأن JDK لا يضمن حدوث آلية الفشل. إذا كنت تستخدم مجموعة من آلية الفشل في بيئة متعددة الخيوط ، فمن المستحسن استخدام "فئات تحت java.util.concurrent" لاستبدال "الفئات تحت حزمة java.util".
لذلك ، في هذا المثال ، تحتاج فقط إلى استبدال ArrayList بالفئة المقابلة ضمن حزمة java.util.concurrent. هذا هو الكود
قائمة ثابتة خاصة <string> list = new ArrayList <Tring> () ؛
استبدال
قائمة ثابتة خاصة <string> list = new CopyOnWriteArrayList <string> () ؛
يمكن حل هذا الحل.
4. مبدأ فاشل
يتم إنشاء الحدث السريع الفشل ، والذي يتم تشغيله من خلال رمي استثناء ConversIdificationException.
لذا ، كيف يرمي ArrayList استثناء ConcurrentModificationException؟
نحن نعلم أن ConcurrentModificationException هو استثناء يتم إلقاؤه عند تشغيل التكرار. دعنا نلقي نظرة على رمز مصدر التكرار أولاً. يتم تنفيذ ITerator of ArrayList في فئة Parent Class Abstract.java. الرمز كما يلي:
حزمة java.util ؛
يمتد قائمة الفئة الملخصية العامة <e> ustrultcollection <e> قائمة الأدوات <e> {... // السمة الفريدة في ملخص // المستخدمة لتسجيل عدد القائمة المعدلة: في كل مرة (إضافة/حذف العمليات ، إلخ) ، modcount+1 transient transient modcount = 0 ؛ // إرجاع Iterator للحصول على القائمة. في الواقع ، هو إعادة كائن ITR. ITerator العامة <e> iterator () {return new itr () ؛ } // ITR هي فئة التنفيذ لـ ITerator (ITerator). الفئة الخاصة ITR تنفذ iterator <e> {int cursor = 0 ؛ int lastret = -1 ؛ // تعديل قيمة السجل للرقم. // في كل مرة يتم إنشاء كائن ITR () جديد ، سيتم حفظ كائن ITR ( // في كل مرة تعبر فيها العناصر الموجودة في القائمة ، ستقارن ما إذا كانت متوقعة AdmodCount و ModCount متساوية ؛ // إذا لم يكن متساويًا ، يتم إلقاء استثناء من ConcurrentModificationException ، مما يؤدي إلى حدث فاشل. int equiventModCount = modCount ؛ boolean public hasnext () {return cursor! = size () ؛ } public e next () {// قبل الحصول على العنصر التالي ، سيتم الحكم على ما إذا كان "modcount يتم حفظه عند إنشاء كائن ITR جديد" و "modcount الحالي" متساوون ؛ // إذا لم يكن ذلك متساويًا ، يتم إلقاء استثناء ConversIdificationException ، مما يؤدي إلى حدث فاشل. checkForComodification () ؛ حاول {e next = get (المؤشر) ؛ LASTRET = CURSOR ++ ؛ العودة بعد ذلك ؛ } catch (indexoutofBoundSexception e) {checkForComodification () ؛ رمي nosuchelementException () ؛ }} public void remove () {if (lastret == -1) رمي جديد alficalstateException () ؛ checkForComodification () ؛ Try {AbstractList.This.Remove (Lastret) ؛ إذا (lastret <cursor) Cursor-- ؛ lastret = -1 ؛ المتوقع expectModCount = modCount ؛ } catch (indexoutofBoundSexception e) {رمي جديد convalentModificationException () ؛ }} النهائي checkForComodification () {if (modCount! = متوقعة) رمي convalentModificationexception () ؛ }} ...} من هذا ، يمكننا أن نجد أن checkforcomodification () يتم تنفيذها عند استدعاء Next () وإزالة (). إذا كان "modcount لا يساوي متوقعًا" ، فسيتم إلقاء استثناء متزامن ، مما يؤدي إلى حدث فاشل.
لفهم آلية الفشل ، نحتاج إلى فهم متى "لا يساوي ModCount متوقعًا"!
من فئة ITR ، نعلم أنه يتم تعيين equiptionModCount إلى modCount عند إنشاء كائن ITR. من خلال ITR ، نعلم أنه لا يمكن تعديل متوقع expecttmodcount لعدم المساواة في modcount. لذلك ، ما يجب التحقق منه هو عندما يتم تعديل ModCount.
بعد ذلك ، دعنا نتحقق من الكود المصدري لـ ArrayList لمعرفة كيفية تعديل ModCount.
حزمة java.util ؛ الطبقة العامة arraylist <e> يمتد قائمة التجريدية <e> قائمة <e> ، عشوائي ، clonable ، java.io.serializable {... // عندما تتغير السعة في القائمة ، وظيفة التزامن المقابلة public void public public (int mincapacity) {modcount ++ ؛ int oldcapacity = elementData.Length ؛ if (mincapacity> oldCapacity) {Object OldData [] = elementData ؛ int newCapacity = (OldCapacity * 3)/2 + 1 ؛ إذا (NewCapacity <mincapacity) NewCapacity = mincapacity ؛ // عادة ما تكون Mincapacity قريبة من الحجم ، لذلك هذا هو الفوز: elementData = Arrays.copyof (ElementData ، NewCapacity) ؛ }} // إضافة عنصر إلى آخر قائمة الانتظار Boolean Add (e e) {// modify modcount insureCapacity (size + 1) ؛ // زيادة modcount !! elementData [size ++] = e ؛ العودة صحيح. } // إضافة عنصر إلى الموقع المحدد public void add (int index ، e element) {if (index> size || index <0) رمي indexoutofboundsexception ("index:"+index+"، الحجم:"+حجم) ؛ // تعديل modcount insureCapacity (size+1) ؛ // زيادة modcount !! System.arrayCopy (elementData ، index ، elementData ، index + 1 ، size - index) ؛ elementData [index] = element ؛ حجم ++ ؛ }. int numNew = A.Length ؛ // تعديل modcount insureCapacity (size + numNew) ؛ // زيادة modcount system.arrayCopy (a ، 0 ، elementData ، size ، numNew) ؛ الحجم += numNew ؛ إرجاع numNew! = 0 ؛ } // حذف العنصر في الموقع المحدد public e remove (int index) {rangecheck (index) ؛ // تعديل modcount modcount ++ ؛ e oldvalue = (e) elementData [index] ؛ int numMoved = size - index - 1 ؛ if (numMoved> 0) system.arrayCopy (elementData ، index+1 ، elementData ، index ، numMoved) ؛ elementData [-size] = null ؛ // دع GC تقوم بعملها على عودة OldValue ؛ } // حذف العناصر بسرعة في الموقع المحدد private void fastremove (int index) {// modify modcount modcount ++ ؛ int numMoved = size - index - 1 ؛ if (numMoved> 0) system.arrayCopy (elementData ، index+1 ، elementData ، index ، numMoved) ؛ elementData [-size] = null ؛ // دع GC تقوم بعملها} // مسح مجموعة الفراغ العام clear () {// تعديل modcount modcount ++ ؛ // دع GC تقوم بعملها (int i = 0 ؛ i <size ؛ i ++) elementData [i] = null ؛ الحجم = 0 ؛ } ...} من هذا ، وجدنا أنه سواء تم إضافة () أو إزالة () أو clear () ، سيتم تغيير قيمة modcount طالما أنها تنطوي على تعديل عدد العناصر في المجموعة.
بعد ذلك ، دعونا نفرز بشكل منهجي كيفية إنتاج فشل. الخطوات كما يلي:
(01) قم بإنشاء اسم ArrayList جديد كـ ArrayList.
(02) إضافة محتوى إلى ArrayList.
(03) قم بإنشاء "مؤشر ترابط" جديد وقراءة قيمة ArrayList مرارًا وتكرارًا من خلال Iterator في "Thread A".
(04) قم بإنشاء "موضوع B" جديد وحذف "عقدة A" في قائمة ArrayList في "Thread B".
(05) في هذا الوقت ، ستحدث أحداث مثيرة للاهتمام.
في مرحلة ما ، "الخيط A" ينشئ التكرار من ArrayList. في هذا الوقت ، "العقدة A" لا تزال موجودة في قائمة ArrayList. عند إنشاء قائمة ArrayList ، متوقعة = modCount (على افتراض أن قيمها ن في هذا الوقت).
في مرحلة ما أثناء عملية اجتياز قائمة ArrayList ، تنفيذ "Thread B" ، ويحذف "Thread B" عقدة "A" في ArrayList. عندما يتم تنفيذ "Thread B" REMOME () لعملية الحذف ، يتم تنفيذ "ModCount ++" في remove () ، ويصبح modcount n+1!
"الموضوع أ" ثم يعبر. عندما تنفذ وظيفة NEXT () ، يتم استدعاء checkForComodification () لمقارنة أحجام "متوقعة electmodcount" و "modcount" ؛ و "المتوقع expectmodcount = n" ، "modcount = n+1" ، لذلك يتم إلقاء استثناء concurrentModificationException ، مما يؤدي إلى حدث فاشل.
في هذه المرحلة ، لدينا فهم كامل لكيفية إنتاج الفشل!
هذا هو ، عندما تعمل مؤشرات ترابط متعددة على نفس المجموعة ، عندما يصل مؤشر ترابط إلى المجموعة ، يتم تغيير محتوى المجموعة بواسطة مؤشرات الترابط الأخرى (أي ، تغييرات مؤشرات الترابط الأخرى تغير قيمة modcount من خلال الإضافة ، إزالة ، مسافة وطرق أخرى) ؛ في هذا الوقت ، سيتم إلقاء استثناء ConcurrentModificationException ، مما يؤدي إلى حدث فاشل.
5. مبدأ حل الفشل
يشرح ما ورد أعلاه "طرق حل آلية الفشل الصاخبة" ويعرف أيضًا "السبب الجذري للفشل". بعد ذلك ، دعنا نتحدث أكثر عن كيفية حل الحدث السريع في حزمة java.util.concurrent.
دعنا نشرح ذلك مع CopyOnWriteArrayList المقابلة لـ ArrayList. دعنا أولاً نلقي نظرة على رمز المصدر لـ CopyWriteArrayList:
package java.util.concurrent ؛ استيراد java.util.*؛ استيراد java.util.concurrent.locks.*؛ استيراد sun.misc.unsafe ؛ من الدرجة العامة copyonwritearraylist <e> قائمة <e> ، clonable ، java.io.serializable {... {إرجاع أساليب تنفيذ Fast-Fail في فئة التجميع الجديدة هي نفسها تقريبًا. لنأخذ أبسط قائمة ArrayList كمثال. محمية عابرة int modcount = 0 ؛ يسجل عدد المرات التي نعدل فيها ArrayList. على سبيل المثال ، عندما ندعو ADD () ، إزالة () ، إلخ لتغيير البيانات ، سيتم تغيير ModCount ++. محمية عابرة int modcount = 0 ؛ يسجل عدد المرات التي نعدل فيها ArrayList. على سبيل المثال ، عندما ندعو ADD () ، إزالة () ، إلخ لتغيير البيانات ، سيتم تغيير ModCount ++. CowiTerator <e> (getArray () ، 0) ؛ } ... COWITERATOR الفئة الثابتة الخاصة <E> تنفذ ListIrator <e> {private Final Object [] Snapshot ؛ المؤشر الخاص cowiTerator الخاص (كائن [] عناصر ، int initialcursor) {cursor = inialCursor ؛ // عند إنشاء جيرلور جديد ، احفظ العناصر في المجموعة إلى مجموعة نسخة جديدة. // بهذه الطريقة ، عندما تتغير بيانات المجموعة الأصلية ، لن تتغير القيم في بيانات النسخ أيضًا. لقطة = عناصر ؛ } public boolean hasnext () {return cursor <snapshot.length ؛ } boolean public hasprevious () {return cursor> 0 ؛ } public e next () {if (! hasnext ()) رمي nosuchelementException () ؛ إرجاع (هـ) لقطة [المؤشر ++] ؛ } public e previour () {if (! hasprevious ()) رمي nosuchelementException () ؛ إرجاع (هـ) لقطة [-المؤشر] ؛ } public int nextIndex () {return cursor ؛ } public int previousIndex () {return cursor-1 ؛ } public void remove () {refl new UnsupportedOperationException () ؛ } مجموعة void العامة (e e) {رمي جديد غير unsupportedOperationException () ؛ } public void add (e e) {refl new UnsupportedOperationException () ؛ }} ...} من هذا يمكننا أن نرى:
(01) على عكس أرث ArrayList من AbstractList ، لا يرث CopyOnWriteArrayList من AbstractList ، فهو ينفذ فقط واجهة القائمة.
(02) يتم تنفيذ Iterator التي تم إرجاعها بواسطة وظيفة iterator () لـ ArrayList في مجردة. بينما يقوم CopyOnWriteArrayList بتنفيذ Iterator نفسه.
(03) عندما يتم استدعاء NEXT () في فئة تنفيذ ITERATAR ARRAYLIST ، "Call CheckForcomodification () لمقارنة أحجام" المتوقع "و" modcount "" ؛ ومع ذلك ، لا يوجد ما يسمى checkforcomodification () في فئة تنفيذ ITerator في CopyOnWriteArrayList ، ولن يتم إلقاء Conversedifificationexception!
6. ملخص
نظرًا لأن HashMap (ArrayList) ليس آمنًا مؤشر ترابط ، إذا قامت مؤشرات الترابط الأخرى بتعديل الخريطة أثناء عملية استخدام التكرار (يشير التعديل هنا إلى التعديل الهيكلي ، وليس فقط لتعديل عناصر محتوى المجموعة) ، فسيتم تنفيذ الإستراتيجية الفاشلة بشكل أساسي لضمان الرؤية بين مؤشرات الترابط. ModCount هو عدد التعديلات. ستتم إضافة هذه القيمة إلى تعديل محتوى HashMap (ArrayList). ثم أثناء تهيئة التكرار ، سيتم تخصيص هذه القيمة إلى expectedModCount المتوقعة.
لكن السلوك السريع غير مضمون ، وبالتالي فإن ممارسة الاعتماد على هذا الاستثناء خاطئ