ก่อนอื่นให้อ่านคำอธิบายโดยละเอียดเกี่ยวกับการซิงโครไนซ์:
ซิงโครไนซ์เป็นคำหลักในภาษา Java เมื่อใช้ในการแก้ไขวิธีการหรือบล็อกโค้ดก็สามารถมั่นใจได้ว่าเธรดส่วนใหญ่จะดำเนินการรหัสในเวลาเดียวกัน
1. เมื่อสองเธรดพร้อมกันเข้าถึงบล็อกรหัสที่ซิงโครไนซ์นี้ (นี้) ซิงโครไนซ์ในวัตถุวัตถุเดียวกันสามารถดำเนินการได้เพียงหนึ่งเธรดภายในหนึ่งครั้ง เธรดอื่นจะต้องรอให้เธรดปัจจุบันเรียกใช้งานบล็อกรหัสนี้ก่อนที่จะสามารถเรียกใช้งานบล็อกรหัส
2. อย่างไรก็ตามเมื่อเธรดหนึ่งเข้าถึงบล็อกรหัสการซิงโครไนซ์ซิงโครไนซ์ (นี้) ของวัตถุเธรดอื่นยังสามารถเข้าถึงบล็อกรหัสซิงโครไนซ์ที่ไม่ซิงโครไนซ์ (นี่) ในวัตถุนั้น
3. เป็นสิ่งสำคัญอย่างยิ่งที่เมื่อเธรดเข้าถึงบล็อกรหัสการซิงโครไนซ์ซิงโครไนซ์ (นี้) ของวัตถุเธรดอื่น ๆ จะถูกบล็อกจากการเข้าถึงบล็อกการซิงโครไนซ์อื่น ๆ ทั้งหมด (นี้) ในวัตถุ
4. ตัวอย่างที่สามยังใช้กับบล็อกรหัสซิงโครนัสอื่น ๆ นั่นคือเมื่อเธรดเข้าถึงบล็อกรหัสซิงโครไนซ์ซิงโครไนซ์ (นี้) ของวัตถุจะได้รับการล็อควัตถุของวัตถุนี้ เป็นผลให้เธรดอื่น ๆ เข้าถึงส่วนรหัสแบบซิงโครนัสทั้งหมดของวัตถุวัตถุถูกบล็อกชั่วคราว
5. กฎข้างต้นใช้กับล็อควัตถุอื่น ๆ
พูดง่ายๆคือซิงโครไนซ์ประกาศล็อคสำหรับเธรดปัจจุบัน เธรดที่มีล็อคนี้สามารถดำเนินการตามคำแนะนำในบล็อกและเธรดอื่น ๆ สามารถรอให้ล็อคได้รับก่อนการดำเนินการเดียวกัน
สิ่งนี้มีประโยชน์มาก แต่ฉันพบสถานการณ์ที่แปลกอีกครั้ง
1. ในคลาสเดียวกันมีสองวิธี: การใช้การประกาศคำหลักที่ซิงโครไนซ์
2. เมื่อดำเนินการอย่างใดอย่างหนึ่งคุณต้องรอวิธีอื่น (การเรียกกลับเธรดแบบอะซิงโครนัส) เพื่อดำเนินการดังนั้นคุณจึงใช้ Countdownlatch เพื่อรอ
3. รหัสถูกแยกออกดังนี้:
การซิงโครไนซ์เป็นโมฆะ A () {countdownlatch = ใหม่ CountdownLatch (1); // ทำบางอย่าง countdownlatch.await ();} void b () {countdownlatch.countdown ();} ใน
วิธี A ถูกดำเนินการโดยเธรดหลักเมธอด B ถูกดำเนินการโดยเธรดแบบอะซิงโครนัสและผลการเรียกใช้การเรียกกลับคือ:
เธรดหลักเริ่มติดอยู่หลังจากดำเนินการวิธี A และไม่ได้ทำอีกต่อไปและมันจะไร้ประโยชน์สำหรับคุณที่จะรอไม่ว่าจะใช้เวลานานแค่ไหน
นี่คือปัญหาการหยุดชะงักแบบคลาสสิก
รอให้ B ดำเนินการ แต่ในความเป็นจริงอย่าคิดว่า B เป็นโทรกลับ B ก็รอให้ A ดำเนินการ ทำไม ซิงโครไนซ์มีบทบาท
โดยทั่วไปเมื่อเราต้องการซิงโครไนซ์บล็อกของรหัสเราจำเป็นต้องใช้ตัวแปรที่ใช้ร่วมกันเพื่อล็อคตัวอย่างเช่น:
ไบต์ [] mutex = ไบต์ใหม่ [0]; เป็นโมฆะ a1 () {ซิงโครไนซ์ (mutex) {// dosomething}} void b1 () {ซิงโครไนซ์ (mutex) {// dosomething}}} หากเนื้อหาของวิธี A และวิธี B ถูกย้ายไปยังบล็อกที่ซิงโครไนซ์ของวิธี A1 และ B1 ตามลำดับมันจะเข้าใจง่าย
หลังจากดำเนินการ A1 แล้วมันจะรอวิธีการ (นับถอยหลัง) B1 ทางอ้อมเพื่อดำเนินการ
อย่างไรก็ตามเนื่องจาก Mutex ใน A1 ไม่ได้เปิดตัวเราจึงเริ่มรอ B1 ในเวลานี้แม้ว่าวิธีการโทรกลับ B1 แบบอะซิงโครนัสจะต้องรอให้ Mutex ปล่อยล็อควิธี B จะไม่ถูกดำเนินการ
สิ่งนี้ทำให้เกิดการหยุดชะงัก!
คำหลักที่ซิงโครไนซ์ที่นี่จะถูกวางไว้ด้านหน้าของวิธีการและฟังก์ชั่นเหมือนกัน เป็นเพียงภาษา Java ช่วยให้คุณซ่อนการประกาศและการใช้ Mutex วิธีการซิงโครไนซ์ที่ใช้ในวัตถุเดียวกันนั้นเหมือนกันดังนั้นแม้แต่การโทรกลับแบบอะซิงโครนัสก็จะทำให้เกิดการหยุดชะงักดังนั้นให้ความสนใจกับปัญหานี้ ข้อผิดพลาดระดับนี้คือคำหลักที่ซิงโครไนซ์ถูกใช้อย่างไม่เหมาะสม อย่าใช้แบบสุ่มและใช้อย่างถูกต้อง
แล้ววัตถุ Mutex ที่มองไม่เห็นคืออะไร?
ตัวอย่างของตัวเองเป็นเรื่องง่ายที่จะคิด เพราะด้วยวิธีนี้ไม่จำเป็นต้องกำหนดวัตถุใหม่และทำการล็อค เพื่อพิสูจน์ความคิดนี้คุณสามารถเขียนโปรแกรมเพื่อพิสูจน์ได้
ความคิดนั้นง่ายมาก กำหนดคลาสและมีสองวิธี หนึ่งถูกประกาศซิงโครไนซ์และอีกอันถูกใช้ซิงโครไนซ์ (นี้) ในร่างกายวิธีการ จากนั้นเริ่มสองเธรดเพื่อเรียกใช้สองวิธีนี้แยกกัน หากการแข่งขันล็อคเกิดขึ้นระหว่างสองวิธี (รอ) สามารถอธิบายได้ว่า mutex ที่มองไม่เห็นในการซิงโครไนซ์ที่ประกาศโดยวิธีนี้เป็นอินสแตนซ์ของตัวเอง
ระดับสาธารณะ multithreadsync {public synchronized void m1 () พ่น interruptedexception {ระบบ out.println ("m1 call"); ด้าย. การนอนหลับ (2000); ระบบ. out.println ("M1 โทรทำ"); } โมฆะสาธารณะ m2 () พ่น InterruptedException {ซิงโครไนซ์ (นี้) {ระบบ out.println ("m2 call"); ด้าย. การนอนหลับ (2000); ระบบ. out.println ("M2 Call Done"); }} โมฆะคงที่สาธารณะหลัก (สตริง [] args) {multithreadsync สุดท้าย thisobj = ใหม่ multithreadsync (); เธรด t1 = เธรดใหม่ () {@Override โมฆะสาธารณะเรียกใช้ () {ลอง {thisobj.m1 (); } catch (interruptedException e) {e.printStackTrace (); - เธรด t2 = ใหม่เธรด () {@Override โมฆะสาธารณะเรียกใช้ () {ลอง {thisobj.m2 (); } catch (interruptedException e) {e.printStackTrace (); - t1.start (); t2.start (); - ผลลัพธ์ผลลัพธ์คือ:
m1 callm1 โทร Donem2 CallM2 Call Done
มีการอธิบายว่าบล็อกซิงค์ของเมธอด M2 กำลังรอการดำเนินการของ M1 สิ่งนี้สามารถยืนยันแนวคิดข้างต้น
ควรสังเกตว่าเมื่อเพิ่มการซิงค์ลงในวิธีการคงที่เนื่องจากเป็นวิธีระดับคลาสวัตถุที่ถูกล็อคเป็นอินสแตนซ์คลาสของคลาสปัจจุบัน คุณยังสามารถเขียนโปรแกรมเพื่อพิสูจน์ได้ ที่นี่ถูกละเว้น
ดังนั้นคำหลักที่ซิงโครไนซ์ของวิธีการสามารถถูกแทนที่โดยอัตโนมัติด้วยการซิงโครไนซ์ (นี้) {} เมื่ออ่านซึ่งง่ายต่อการเข้าใจ
โมฆะวิธี () {วิธีการที่ซิงโครไนซ์เป็นโมฆะ () {ซิงโครไนซ์ (นี่) {// biz code // biz code} ------ >>>}}} การมองเห็นหน่วยความจำจากการซิงโครไนซ์
ใน Java เราทุกคนรู้ว่าคำหลักที่ซิงโครไนซ์สามารถใช้เพื่อใช้การยกเว้นซึ่งกันและกันระหว่างเธรด แต่เรามักจะลืมว่ามันมีฟังก์ชั่นอื่นนั่นคือเพื่อให้แน่ใจว่าการมองเห็นตัวแปรในหน่วยความจำ - นั่นคือเมื่อสองเธรดที่อ่านและเขียนตัวแปรการอ่านล่าสุด
ตัวอย่างเช่นตัวอย่างต่อไปนี้:
ระดับสาธารณะระดับ novisibility {บูลีนคงที่ส่วนตัวพร้อม = false; หมายเลข int คงที่ส่วนตัว = 0; Private Static Class ReaderThread ขยายเธรด {@Override โมฆะสาธารณะเรียกใช้ () {ในขณะที่ (พร้อม) {thread.yield (); // สนับสนุน CPU เพื่อให้เธรดอื่นทำงาน} system.out.println (หมายเลข); }} โมฆะคงที่สาธารณะหลัก (สตริง [] args) {ใหม่ readerThread (). start (); หมายเลข = 42; พร้อม = จริง; -คุณคิดว่าการอ่านเธรดจะส่งออกอะไร? 42? ภายใต้สถานการณ์ปกติ 42 จะถูกส่งออก อย่างไรก็ตามเนื่องจากปัญหาการเรียงลำดับใหม่เธรดการอ่านอาจส่งออก 0 หรือเอาต์พุตไม่มีอะไร
เรารู้ว่าคอมไพเลอร์อาจสั่งซื้อรหัสใหม่เมื่อรวบรวมรหัส Java ลงในไบต์และ CPU อาจจัดลำดับคำแนะนำใหม่อีกครั้งเมื่อดำเนินการตามคำแนะนำของเครื่อง ตราบใดที่การจัดลำดับใหม่ไม่ทำลายความหมายของโปรแกรม-
ในเธรดเดียวตราบใดที่การจัดลำดับใหม่ไม่ส่งผลกระทบต่อผลการดำเนินการของโปรแกรมก็ไม่สามารถรับประกันได้ว่าการดำเนินการในนั้นจะต้องดำเนินการตามลำดับที่ระบุโดยโปรแกรมแม้ว่าการจัดลำดับใหม่อาจมีผลกระทบอย่างมีนัยสำคัญต่อเธรดอื่น ๆ
ซึ่งหมายความว่าการดำเนินการของคำสั่ง "ready = true" อาจมีความสำคัญกว่าการดำเนินการของคำสั่ง "number = 42" ในกรณีนี้เธรดการอ่านอาจส่งออกค่าเริ่มต้นของหมายเลข 0
ภายใต้โมเดลหน่วยความจำ Java ปัญหาการจัดเรียงใหม่จะนำไปสู่ปัญหาการมองเห็นหน่วยความจำดังกล่าว ภายใต้โมเดลหน่วยความจำ Java แต่ละเธรดมีหน่วยความจำการทำงานของตัวเอง (ส่วนใหญ่เป็นแคช CPU หรือการลงทะเบียน) และการดำเนินการของตัวแปรจะดำเนินการในหน่วยความจำการทำงานของตัวเองในขณะที่การสื่อสารระหว่างเธรดทำได้ผ่านการซิงโครไนซ์ระหว่างหน่วยความจำหลักและหน่วยความจำการทำงานของเธรด
ตัวอย่างเช่นสำหรับตัวอย่างข้างต้นเธรดการเขียนได้อัปเดตหมายเลขสำเร็จเป็น 42 และพร้อมที่จะเป็นจริง แต่มีแนวโน้มว่าเธรดการเขียนจะซิงโครไนซ์หมายเลขกับหน่วยความจำหลักเท่านั้น (อาจเป็นเพราะบัฟเฟอร์การเขียนของ CPU) ส่งผลให้ค่าพร้อมที่อ่านต่อไป
หากเราใช้คำหลักที่ซิงโครไนซ์เพื่อซิงโครไนซ์จะไม่มีปัญหาดังกล่าว
ระดับสาธารณะระดับ novisibility {บูลีนคงที่ส่วนตัวพร้อม = false; หมายเลข int คงที่ส่วนตัว = 0; ล็อควัตถุคงที่ส่วนตัว = วัตถุใหม่ (); Private Static Class ReaderThread ขยายเธรด {@Override โมฆะสาธารณะเรียกใช้ () {ซิงโครไนซ์ (ล็อค) {ในขณะที่ (พร้อม) {thread.yield (); } system.out.println (หมายเลข); }} โมฆะคงที่สาธารณะหลัก (สตริง [] args) {ซิงโครไนซ์ (ล็อค) {ใหม่ readerThread (). start (); หมายเลข = 42; พร้อม = จริง; - นี่เป็นเพราะโมเดลหน่วยความจำ Java ให้การรับประกันต่อไปนี้สำหรับความหมายที่ซิงโครไนซ์
นั่นคือเมื่อ Threada ปล่อยล็อค M ตัวแปรที่เขียนไว้ (เช่น X และ Y ซึ่งมีอยู่ในหน่วยความจำการทำงาน) จะถูกซิงโครไนซ์กับหน่วยความจำหลัก เมื่อ ThreadB ใช้สำหรับการล็อค M เดียวกันหน่วยความจำการทำงานของ ThreadB จะถูกตั้งค่าเป็นไม่ถูกต้องจากนั้น ThreadB จะโหลดตัวแปรที่ต้องการเข้าถึงจากหน่วยความจำหลักในหน่วยความจำการทำงาน (ในเวลานี้ x = 1, y = 1 เป็นค่าล่าสุดที่แก้ไขในเธรด) ด้วยวิธีนี้การสื่อสารระหว่างเธรดจาก Threada ถึง Threadb จะทำได้
นี่เป็นหนึ่งในกฎที่เกิดขึ้นก่อนที่ JSR133 JSR133 กำหนดชุดของกฎที่เกิดขึ้นก่อนหน้านี้สำหรับโมเดลหน่วยความจำ Java
ในความเป็นจริงชุดของการเกิดขึ้นก่อนหน้านี้จะกำหนดการมองเห็นหน่วยความจำระหว่างการดำเนินการ หากการดำเนินการเกิดขึ้นก่อนการดำเนินการ B ผลการดำเนินการของการดำเนินการ (เช่นการเขียนไปยังตัวแปร) จะต้องมองเห็นได้เมื่อทำการดำเนินการ B
เพื่อให้เข้าใจอย่างลึกซึ้งยิ่งขึ้นเกี่ยวกับกฎเหล่านี้ก่อนที่จะลองมาเป็นตัวอย่าง:
// รหัสที่แชร์โดยเธรด a และ b ล็อควัตถุ = วัตถุใหม่ (); int a = 0; int b = 0; int c = 0; // เธรด a, เรียกรหัสต่อไปนี้ซิงโครไนซ์ (ล็อค) {a = 1; // 1 b = 2; // 2} // 3C = 3; // 4 // เธรด B, เรียกรหัสต่อไปนี้ซิงโครไนซ์ (ล็อค) {// 5 System.out.println (a); // 6 system.out.println (b); // 7 System.out.println (c); // 8}เราสมมติว่าเธรด A รันก่อนกำหนดค่าให้กับตัวแปรสามตัว A, B และ C ตามลำดับ (หมายเหตุ: การกำหนดตัวแปร A, B ดำเนินการในบล็อกคำสั่งซิงโครนัส) จากนั้นเธรด B จะทำงานอีกครั้งอ่านค่าของตัวแปรทั้งสามนี้และพิมพ์ออกมา ดังนั้นค่าของตัวแปร A, B และ C พิมพ์ออกมาโดยเธรด B คืออะไร?
ตามกฎการเธรดเดียวในการดำเนินการของเธรด A เราสามารถรับการดำเนินการ 1 ครั้งก่อนการดำเนินการ 2 การดำเนินการ 2 ครั้งเกิดขึ้นก่อนการดำเนินการ 3 ครั้งและการดำเนินการ 3 ครั้งเกิดขึ้นก่อนการดำเนินการ 4 ครั้ง ในทำนองเดียวกันในการดำเนินการของเธรด B, 5 การดำเนินการเกิดขึ้นก่อนการดำเนินการ 6 ครั้งการดำเนินการ 6 ครั้งเกิดขึ้นก่อนการดำเนินการ 7 ครั้งและการดำเนินการ 7 ครั้งเกิดขึ้นก่อนการดำเนินการ 8 ครั้ง ตามหลักการของการปลดล็อคและล็อคของจอภาพการดำเนินการทั้ง 3 (การปลดล็อค) จะเกิดขึ้นก่อนการดำเนินการ 5 ครั้ง (การล็อค) ตามกฎสกรรมกริยาเราสามารถสรุปได้ว่าการดำเนินการ 1 และ 2 เกิดขึ้นก่อนการดำเนินการ 6, 7 และ 8
ตามความหมายของหน่วยความจำของการเกิดขึ้นก่อนหน้านี้ผลการดำเนินการของการดำเนินงาน 1 และ 2 สามารถมองเห็นได้จากการดำเนินการ 6, 7 และ 8 ดังนั้นในเธรด B, A และ B ถูกพิมพ์จะต้องเป็น 1 และ 2 สำหรับการดำเนินการ 4 และการดำเนินการ 8 ของตัวแปร C เราไม่สามารถอนุมานได้ว่าการดำเนินการ 4 เกิดขึ้นก่อนการดำเนินการ 8 ตามที่เกิดขึ้นก่อนที่จะมีกฎ ดังนั้นในเธรด B ตัวแปรที่เข้าถึงไป C อาจยังคงเป็น 0 ไม่ใช่ 3