1. บทนำสู่ความล้มเหลวอย่างรวดเร็ว
"Fast Fail" เรียกอีกอย่างว่า Fail-Fast ซึ่งเป็นกลไกการตรวจจับข้อผิดพลาดสำหรับคอลเลกชัน Java เมื่อเธรดวนซ้ำผ่านคอลเลกชันเธรดอื่น ๆ จะไม่ได้รับอนุญาตให้แก้ไขการรวบรวมโครงสร้าง
ตัวอย่างเช่น: สมมติว่ามีสองเธรด (เธรด 1 และเธรด 2) และเธรด 1 สำรวจองค์ประกอบในชุด A ผ่านตัววนซ้ำ ในบางจุดเธรด 2 จะปรับเปลี่ยนโครงสร้างของชุด A (การปรับเปลี่ยนโครงสร้างแทนที่จะแก้ไขเนื้อหาขององค์ประกอบที่ตั้งไว้) จากนั้นโปรแกรมจะโยนข้อยกเว้นพร้อมกัน Exception พร้อมกัน
พฤติกรรมความล้มเหลวอย่างรวดเร็วของตัววนซ้ำไม่สามารถรับประกันได้ไม่สามารถรับประกันได้ว่าข้อผิดพลาดจะเกิดขึ้นดังนั้นควรใช้พร้อมกันเพื่อตรวจสอบข้อบกพร่องเท่านั้น
คลาสคอลเลกชันทั้งหมดในแพ็คเกจ java.util ล้มเหลวอย่างรวดเร็วในขณะที่คลาสคอลเลกชันในแพ็คเกจ java.util.concurrent ล้มเหลวอย่างปลอดภัย
ตัววนซ้ำที่ล้มเหลวอย่างรวดเร็วโยนเครื่องแก้ไขพร้อมกันอย่างรวดเร็วในขณะที่ตัววนซ้ำที่ล้มเหลวอย่างปลอดภัยไม่เคยโยนข้อยกเว้นนี้อย่างปลอดภัย
2 ตัวอย่างที่ล้มเหลวอย่างรวดเร็ว
รหัสตัวอย่าง: (fastfailtest.java)
นำเข้า java.util.*; นำเข้า Java.util.concurrent.*;/** @desc โปรแกรมทดสอบสำหรับ Fast-Fail ใน Java Collection * * เงื่อนไขสำหรับเหตุการณ์ Fast-Fail: เมื่อหลายเธรดทำงานในคอลเลกชันถ้าหนึ่งในเธรดจะสำรวจคอลเลกชันผ่านตัววนซ้ำเนื้อหาของคอลเลกชันจะเปลี่ยนไปโดยเธรดอื่น ๆ ข้อยกเว้นที่เกิดขึ้นพร้อมกัน Exception จะถูกโยนลงไป * โซลูชัน Fast-Fail: หากคุณประมวลผลผ่านคลาสที่เกี่ยวข้องภายใต้แพ็คเกจคอลเลกชัน Util.Concurrent เหตุการณ์ Fast-Fail จะไม่ถูกสร้างขึ้น * * ในตัวอย่างนี้ทั้งสองกรณีของ ArrayList และ CopyOnWriteArrayList ได้รับการทดสอบตามลำดับ ArrayList จะสร้างเหตุการณ์ Fast-Fail ในขณะที่ CopyOnWriteArrayList จะไม่สร้างเหตุการณ์ Fast-Fail * (01) เมื่อใช้ ArrayList เหตุการณ์ Fast-Fail จะถูกสร้างขึ้นและข้อยกเว้นพร้อมกันพร้อมกันจะถูกโยนออกไป คำจำกัดความมีดังนี้: * รายการคงที่ส่วนตัว <String> list = new ArrayList <String> (); * (02) เมื่อใช้ CopyOnWriteArrayList เหตุการณ์ Fast-Fail จะไม่ถูกสร้างขึ้น คำจำกัดความมีดังนี้: * รายการคงที่ส่วนตัว <String> list = new CopyOnWriteArrayList <String> (); * * @author Skywang */คลาสสาธารณะ FastFailTest {รายการคงที่ส่วนตัว <String> list = new ArrayList <String> (); // รายการคงที่ส่วนตัว <String> list = new CopyOnWriteArrayList <String> (); โมฆะคงที่สาธารณะหลัก (สตริง [] args) {// เริ่มสองเธรดในเวลาเดียวกันเพื่อทำงานในรายการ! threadone ใหม่ (). start (); ใหม่ Threadtwo (). start (); } private static void printall () {system.out.println (""); ค่าสตริง = null; Iterator iter = list.iterator (); ในขณะที่ (iter.hasnext ()) {value = (string) iter.next (); System.out.print (value+","); }} /*** เพิ่ม 0,1,2,3,4,5 ในรายการในทางกลับกัน หลังจากเพิ่มตัวเลขให้วนซ้ำผ่าน printall () */ ชั้นเรียนแบบคงที่แบบคงที่ threadone ขยายเธรด {โมฆะสาธารณะ run () {int i = 0; ในขณะที่ (i <6) {list.add (string.valueof (i)); printall (); i ++; }}} /*** เพิ่ม 10,11,12,13,14,15 ในรายการในทางกลับกัน หลังจากเพิ่มแต่ละหมายเลขแล้วมันจะสำรวจรายการทั้งหมดผ่าน printall () */ คลาสคงที่คลาสคงที่ Threadtwo ขยายเธรด {public void run () {int i = 10; ในขณะที่ (i <16) {list.add (string.valueof (i)); printall (); i ++; - เรียกใช้รหัสเป็นผลลัพธ์และโยนข้อยกเว้น java.util.concurrentModificationException! นั่นคือเหตุการณ์ที่ล้มเหลวอย่างรวดเร็วถูกสร้างขึ้น!
คำอธิบายผลลัพธ์
(01) ใน fastfailtest เริ่มสองเธรดในเวลาเดียวกันเพื่อใช้งานรายการผ่าน threadone ใหม่ (). start () และ Threadtwo ใหม่ () เริ่มต้น ()
เธรดเธรด: เพิ่ม 0, 1, 2, 3, 4, 5 ไปยังรายการ หลังจากเพิ่มแต่ละหมายเลขแล้วรายการทั้งหมดจะถูกสำรวจผ่าน Printall ()
Threadtwo Thread: เพิ่ม 10, 11, 12, 13, 14, 15 ไปยังรายการ หลังจากเพิ่มแต่ละหมายเลขแล้วรายการทั้งหมดจะถูกสำรวจผ่าน Printall ()
(02) เมื่อเธรดผ่านรายการเนื้อหาของรายการจะถูกเปลี่ยนโดยเธรดอื่น ข้อยกเว้นพร้อมกันของการรับรู้พร้อมกันจะถูกโยนส่งผลให้เกิดเหตุการณ์ที่ล้มเหลวอย่างรวดเร็ว
3. โซลูชันที่ล้มเหลวอย่างรวดเร็ว
กลไกที่ล้มเหลวอย่างรวดเร็วเป็นกลไกการตรวจจับข้อผิดพลาด มันสามารถใช้ในการตรวจจับข้อผิดพลาดเท่านั้นเนื่องจาก JDK ไม่รับประกันว่ากลไกที่ล้มเหลวจะเกิดขึ้น หากคุณใช้คอลเลกชันของกลไกที่ล้มเหลวอย่างรวดเร็วในสภาพแวดล้อมแบบมัลติเธรดขอแนะนำให้ใช้ "คลาสภายใต้แพ็คเกจ java.util.concurrent" เพื่อแทนที่ "คลาสภายใต้แพ็คเกจ java.util"
ดังนั้นในตัวอย่างนี้คุณจะต้องแทนที่ ArrayList ด้วยคลาสที่เกี่ยวข้องภายใต้แพ็คเกจ java.util.concurrent นั่นคือรหัส
รายการคงที่ส่วนตัว <String> list = new ArrayList <String> ();
เปลี่ยนด้วย
รายการคงที่ส่วนตัว <String> list = new CopyOnWriteArrayList <String> ();
โซลูชันนี้สามารถแก้ไขได้
4. หลักการล้มเหลวอย่างรวดเร็ว
เหตุการณ์ Fail-Fast ถูกสร้างขึ้นซึ่งถูกกระตุ้นโดยการโยนข้อยกเว้น ExperationModificationException พร้อมกัน
ดังนั้น ArrayList จะโยนข้อยกเว้น Excession Exception พร้อมกันได้อย่างไร?
เรารู้ว่าพร้อมกันกับ Exception เป็นข้อยกเว้นที่ถูกโยนทิ้งเมื่อใช้งานตัววนซ้ำ มาดูซอร์สโค้ดตัววนซ้ำก่อน ตัววนซ้ำของ ArrayList ถูกนำมาใช้ในคลาสหลัก AbstractList.java รหัสมีดังนี้:
แพ็คเกจ java.util;
บทคัดย่อระดับสาธารณะบทคัดย่อบทคัดย่อ <e> ขยายบทคัดย่อ abstractCollection <E> รายการดำเนินการ <E> {... // แอตทริบิวต์ที่ไม่ซ้ำกันใน AbstractList // ใช้เพื่อบันทึกจำนวนรายการที่แก้ไขแล้ว: ทุกครั้ง (เพิ่ม/ลบการดำเนินการ ฯลฯ ), ModCount+1 // ส่งคืน Iterator สำหรับรายการ ในความเป็นจริงมันคือการส่งคืนวัตถุ ITR ตัววนซ้ำสาธารณะ <e> iterator () {return ใหม่ itr (); } // itr เป็นคลาสการใช้งานของตัววนซ้ำ (ตัววนซ้ำ) คลาสส่วนตัว ITR ใช้ Iterator <e> {int cursor = 0; int lastret = -1; // แก้ไขค่าบันทึกของหมายเลข // ทุกครั้งที่มีการสร้างวัตถุ ITR () ใหม่ ModCount ที่สอดคล้องกันเมื่อวัตถุใหม่ถูกสร้างขึ้นจะถูกบันทึก // ทุกครั้งที่คุณสำรวจองค์ประกอบในรายการคุณจะเปรียบเทียบว่าคาดว่า ModCount และ ModCount นั้นเท่าเทียมกันหรือไม่ // หากไม่เท่ากันข้อยกเว้นพร้อมกันของการรับรู้พร้อมกันจะถูกโยนลงไปส่งผลให้เกิดเหตุการณ์ที่ล้มเหลวอย่างรวดเร็ว int คาดหวัง modCount = modCount; บูลีนสาธารณะ hasnext () {กลับเคอร์เซอร์! = size (); } สาธารณะ e ถัดไป () {// ก่อนที่จะได้รับองค์ประกอบถัดไปมันจะถูกตัดสินว่า "modcount บันทึกเมื่อสร้างวัตถุ ITR ใหม่" และ "ModCount ปัจจุบัน" เท่ากันหรือไม่ // หากไม่เท่ากันข้อยกเว้นของการรับรู้ร่วมกันพร้อมกันจะถูกโยนลงไปส่งผลให้เกิดเหตุการณ์ที่ล้มเหลวอย่างรวดเร็ว checkforcomodification (); ลอง {e next = get (เคอร์เซอร์); Lastret = เคอร์เซอร์ ++; กลับมาถัดไป; } catch (indexoutofboundsexception e) {checkforcomodification (); โยน nosuchelementexception ใหม่ (); }} โมฆะสาธารณะลบ () {ถ้า (LASTRET == -1) โยน unlueLstateException ใหม่ (); checkforcomodification (); ลอง {Abstractlist.his.remove (Lastret); ถ้าเคอร์เซอร์ (Lastret <เคอร์เซอร์)-; Lastret = -1; คาดว่า ModCount = ModCount; } catch (indexoutofboundsexception e) {โยนใหม่พร้อมกันใหม่ Exception (); }} void สุดท้าย checkForComodification () {ถ้า (modcount! = คาดหวัง modcount) โยนใหม่พร้อมกัน modificationException (); - จากนี้เราสามารถพบว่า checkforcomodification () จะถูกดำเนินการเมื่อเรียกว่า next () และลบ () หาก "ModCount ไม่เท่ากับที่คาดหวัง" ข้อยกเว้นพร้อมกันพร้อมกันจะถูกโยนทิ้งส่งผลให้เกิดเหตุการณ์ที่ล้มเหลวอย่างรวดเร็ว
เพื่อให้เข้าใจถึงกลไกที่ล้มเหลวอย่างรวดเร็วเราต้องเข้าใจเมื่อ "Modcount ไม่เท่ากับที่คาดหวังว่า"!
จากคลาส ITR เรารู้ว่าคาดว่าจะได้รับมอบหมายให้ ModCount เมื่อสร้างวัตถุ ITR ผ่าน ITR เรารู้ว่าไม่สามารถแก้ไขได้ที่คาดว่าจะไม่ได้รับการแก้ไข ดังนั้นสิ่งที่ต้องตรวจสอบคือเมื่อ modcount จะถูกแก้ไข
ถัดไปลองตรวจสอบซอร์สโค้ดของ ArrayList เพื่อดูว่า ModCount ได้รับการแก้ไขอย่างไร
แพ็คเกจ java.util; arraylist คลาสสาธารณะ <e> ขยายบทคัดย่อ <e> ใช้รายการ <e>, สุ่ม, cloneable, java.io.serializable {... // เมื่อความสามารถเปลี่ยนแปลงในรายการ, ฟังก์ชั่นการซิงโครไนซ์ที่สอดคล้องกัน int oldcapacity = elementData.length; if (mincapacity> oldcapacity) {object olddata [] = elementData; int newCapacity = (oldcapacity * 3)/2 + 1; if (newCapacity <mincapacity) newCapacity = mincapacity; // mincapacity มักจะใกล้เคียงกับขนาดดังนั้นนี่คือการชนะ: elementData = arrays.copyof (ElementData, newCapacity); }} // เพิ่มองค์ประกอบลงในบูลีนสาธารณะสุดท้ายเพิ่ม (e e) {// แก้ไข modcount ensurecapacity (ขนาด + 1); // เพิ่ม modcount !! ElementData [ขนาด ++] = E; กลับมาจริง; } // เพิ่มองค์ประกอบลงในตำแหน่งที่ระบุโมฆะสาธารณะเพิ่ม (ดัชนี int, องค์ประกอบ e) {ถ้า (ดัชนี> ขนาด || ดัชนี <0) โยน indexoutofboundsexception ใหม่ ("ดัชนี:"+ดัชนี+", ขนาด:"+ขนาด); // แก้ไข modcount ensurecapacity (ขนาด+1); // เพิ่ม modcount !! System.arrayCopy (ElementData, INDEX, ElementData, ดัชนี + 1, ขนาด - ดัชนี); ElementData [ดัชนี] = องค์ประกอบ; ขนาด ++; } // เพิ่มคอลเลกชันบูลีนสาธารณะ addall (คอลเลกชัน <? ขยาย e> c) {object [] a = c.toarray (); int numnew = a.length; // แก้ไข modcount ensurecapacity (ขนาด + numnew); // เพิ่ม modcount system.arraycopy (a, 0, elementData, ขนาด, numnew); ขนาด += numnew; ส่งคืน numnew! = 0; } // ลบองค์ประกอบในตำแหน่งที่ระบุสาธารณะ e ลบ (ดัชนี int) {rangecheck (ดัชนี); // แก้ไข modcount modcount ++; e oldValue = (e) elementData [ดัชนี]; int nummoved = size - ดัชนี - 1; if (nummoved> 0) system.arraycopy (elementData, ดัชนี+1, elementData, ดัชนี, nummoved); ElementData [-size] = null; // ให้ GC ทำงานคืน OldValue; } // ลบองค์ประกอบอย่างรวดเร็วในตำแหน่งที่ระบุโมฆะส่วนตัว fastremove (ดัชนี int) {// แก้ไข modcount modcount ++; int nummoved = size - ดัชนี - 1; if (nummoved> 0) system.arraycopy (elementData, ดัชนี+1, elementData, ดัชนี, nummoved); ElementData [-size] = null; // ให้ GC ทำงาน} // ล้างโมฆะสาธารณะคอลเลกชัน Clear () {// modify modcount modcount ++; // ให้ gc ทำงานสำหรับ (int i = 0; i <size; i ++) elementData [i] = null; ขนาด = 0; - จากนี้เราพบว่าไม่ว่าจะเป็น add (), remove (), หรือ clear (), ค่าของ modcount จะเปลี่ยนไปตราบเท่าที่มันเกี่ยวข้องกับการปรับเปลี่ยนจำนวนองค์ประกอบในชุด
ถัดไปให้เรียงลำดับอย่างเป็นระบบว่า Fail-Fast ผลิตได้อย่างไร ขั้นตอนมีดังนี้:
(01) สร้างชื่อ ArrayList ใหม่เป็น ArrayList
(02) เพิ่มเนื้อหาลงใน ArrayList
(03) สร้าง "เธรด A" ใหม่และอ่านค่าของ ArrayList ซ้ำ ๆ ผ่านตัววนซ้ำใน "เธรด A"
(04) สร้าง "เธรด B" ใหม่และลบ "Node A" ใน ArrayList ใน "Thread B"
(05) ในเวลานี้เหตุการณ์ที่น่าสนใจจะเกิดขึ้น
ในบางจุด "เธรด A" สร้างตัววนซ้ำของ ArrayList ในเวลานี้ "โหนด" ยังคงมีอยู่ใน ArrayList เมื่อสร้าง arrayList คาดว่าจะได้รับ = modCount (สมมติว่าค่าของพวกเขาเป็น n ในเวลานี้)
เมื่อถึงจุดหนึ่งในระหว่างกระบวนการสำรวจ ArrayList, "Thread B" ดำเนินการและ "Thread B" DELETES "NODE A" ใน ArrayList เมื่อ "เธรด B" ดำเนินการลบ () สำหรับการลบการดำเนินการ "ModCount ++" จะถูกดำเนินการในลบ () และ modCount กลายเป็น n+1!
"Thread a" จากนั้นเดินทางไป เมื่อมันดำเนินการฟังก์ชั่นถัดไป () CheckForComodification () จะถูกเรียกให้เปรียบเทียบขนาดของ "คาดว่าจะได้รับ" และ "modcount"; และ "คาดหวัง modcount = n", "modCount = n+1" ดังนั้นข้อยกเว้นที่เกิดขึ้นพร้อมกัน exception ถูกโยนลงส่งผลให้เกิดเหตุการณ์ที่ล้มเหลวอย่างรวดเร็ว
ณ จุดนี้เรามีความเข้าใจอย่างถ่องแท้ว่าการผลิตล้มเหลวอย่างรวดเร็ว!
นั่นคือเมื่อหลายเธรดทำงานในชุดเดียวกันเมื่อเธรดเข้าถึงชุดเนื้อหาของชุดจะถูกเปลี่ยนโดยเธรดอื่น (นั่นคือเธรดอื่น ๆ จะเปลี่ยนค่าของ modcount ผ่านการเพิ่มลบและวิธีอื่น ๆ ); ในเวลานี้ข้อยกเว้นการแก้ไขพร้อมกันพร้อมกันจะถูกโยนลงไปส่งผลให้เกิดเหตุการณ์ที่ล้มเหลวอย่างรวดเร็ว
5. หลักการในการแก้ปัญหาที่ล้มเหลวอย่างรวดเร็ว
ข้างต้นอธิบาย "วิธีการในการแก้กลไกที่ล้มเหลวอย่างรวดเร็ว" และยังรู้ว่า "สาเหตุที่แท้จริงของความล้มเหลวอย่างรวดเร็ว" ถัดไปเรามาพูดคุยเพิ่มเติมเกี่ยวกับวิธีการแก้เหตุการณ์ที่ล้มเหลวอย่างรวดเร็วในแพ็คเกจ java.util.concurrent
มาอธิบายด้วย copyonWriteArrayList ที่สอดคล้องกับ ArrayList ก่อนอื่นมาดูซอร์สโค้ดของ CopyOnWriteArrayList:
แพ็คเกจ java.util.concurrent; นำเข้า java.util.*; นำเข้า java.util.concurrent.locks.*; นำเข้า sun.misc.unsafe; copyonwritearraylist <e> นำเข้ามาในรายการ Iterator () {ส่งคืนวิธีการใช้งาน Fast-Fail ในคลาสคอลเลกชันใหม่เกือบจะเหมือนกัน ลองใช้ arraylist ที่ง่ายที่สุดเป็นตัวอย่าง ได้รับการป้องกัน transient int modCount = 0; บันทึกจำนวนครั้งที่เราแก้ไข ArrayList ตัวอย่างเช่นเมื่อเราเรียกเพิ่ม (), ลบ () ฯลฯ เพื่อเปลี่ยนข้อมูล ModCount ++ จะมีการเปลี่ยนแปลง ได้รับการป้องกัน transient int modCount = 0; บันทึกจำนวนครั้งที่เราแก้ไข ArrayList ตัวอย่างเช่นเมื่อเราเรียกเพิ่ม (), ลบ () ฯลฯ เพื่อเปลี่ยนข้อมูล ModCount ++ จะมีการเปลี่ยนแปลง Cowiterator <E> (getArray (), 0); } ... cowiterator คลาสคงที่ส่วนตัว <E> ใช้ listiterator <e> {วัตถุสุดท้ายส่วนตัว [] snapshot; เคอร์เซอร์ int ส่วนตัว; cowiterator ส่วนตัว (วัตถุ [] องค์ประกอบ, intialCursor int) {Cursor = InitialCursor; // เมื่อสร้าง cowiterator ใหม่ให้บันทึกองค์ประกอบในคอลเลกชันไปยังอาร์เรย์สำเนาใหม่ // ด้วยวิธีนี้เมื่อข้อมูลของชุดดั้งเดิมเปลี่ยนแปลงค่าในข้อมูลการคัดลอกจะไม่เปลี่ยนแปลงเช่นกัน snapshot = องค์ประกอบ; } บูลีนสาธารณะ hasnext () {ส่งคืนเคอร์เซอร์ <snapshot.length; } บูลีนสาธารณะ hasprevious () {กลับเคอร์เซอร์> 0; } สาธารณะ e ถัดไป () {ถ้า (! hasnext ()) โยน nosuchelementException ใหม่ (); return (e) snapshot [เคอร์เซอร์ ++]; } สาธารณะก่อนหน้า () {ถ้า (! hasprevious ()) โยน nosuchelementException ใหม่ (); return (e) snapshot [-เคอร์เซอร์]; } public int nextindex () {กลับเคอร์เซอร์; } public int preventIndex () {return cursor-1; } โมฆะสาธารณะลบ () {โยน unsupportedoperationException ใหม่ (); } ชุดโมฆะสาธารณะ (e e) {โยน unsupportedoperationException ใหม่ (); } โมฆะสาธารณะเพิ่ม (e e) {โยน unsupportedoperationException ใหม่ (); - จากนี้เราจะเห็น:
(01) ซึ่งแตกต่างจาก ArrayList ที่สืบทอดมาจาก AbstractList, CopyOnWriteArrayList ไม่ได้รับมรดกจาก AbstractList แต่ใช้เฉพาะอินเตอร์เฟสรายการเท่านั้น
(02) ตัววนซ้ำที่ส่งคืนโดยฟังก์ชั่น Iterator () ของ ArrayList ถูกนำมาใช้ใน AbstractList; ในขณะที่ CopyOnWriteArrayList ใช้ตัววนซ้ำเอง
(03) เมื่อ Next () ถูกเรียกในคลาสการใช้งานตัววนซ้ำของ ArrayList "Call CheckForComodification () เพื่อเปรียบเทียบขนาดของ 'BESTEMMODCOUNT' และ 'MODCOUNT'"; อย่างไรก็ตามไม่มีสิ่งที่เรียกว่า checkforcomodification () ในคลาสการใช้งานตัววนซ้ำของ copyonWriteArrayList และการเกิดขึ้นพร้อมกันจะไม่ถูกโยนทิ้ง!
6. สรุป
เนื่องจาก HASHMAP (ArrayList) ไม่ปลอดภัยกับเธรดหากเธรดอื่น ๆ แก้ไขแผนที่ในระหว่างกระบวนการใช้ตัววนซ้ำ (การปรับเปลี่ยนที่นี่หมายถึงการปรับเปลี่ยนโครงสร้างไม่ใช่เพียงแค่ปรับเปลี่ยนองค์ประกอบของเนื้อหาการรวบรวม) จากนั้นการตรวจสอบความเป็นไปได้ Modcount คือจำนวนการดัดแปลง ค่านี้จะถูกเพิ่มเข้าไปในการปรับเปลี่ยนเนื้อหา HASHMAP (ArrayList) จากนั้นในระหว่างการเริ่มต้นของตัววนซ้ำค่านี้จะถูกกำหนดให้กับตัววนซ้ำที่คาดหวังของ iterator
แต่พฤติกรรมที่ล้มเหลวไม่ได้รับการรับประกันดังนั้นการฝึกฝนการพึ่งพาข้อยกเว้นนี้จึงผิด