0. รหัสคำถามบุกเบิก
รหัสต่อไปนี้แสดงให้เห็นถึงตัวนับที่สองเธรดสะสม I ในเวลาเดียวกันแต่ละตัวทำการแสดง 1,000,000 ครั้ง ผลลัพธ์ที่เราคาดหวังคือ i = 2000000 แน่นอน อย่างไรก็ตามหลังจากที่เราดำเนินการหลายครั้งเราจะพบว่าค่าของฉันจะน้อยกว่า 2000000 เสมอนี่เป็นเพราะเมื่อสองเธรดเขียนฉันในเวลาเดียวกันผลลัพธ์ของหนึ่งในเธรดจะเขียนทับอีกเธรด
Class Public Accateingsync ใช้งาน Runnable {Static Int i = 0; โมฆะสาธารณะเพิ่ม () {i ++; } @Override โมฆะสาธารณะเรียกใช้ () {สำหรับ (int j = 0; j <1000000; j ++) {เพิ่ม (); }} โมฆะคงที่สาธารณะหลัก (สตริง [] args) พ่น InterruptedException {accateingsync accateingsync = accateingsync () ใหม่ (); เธรด t1 = เธรดใหม่ (accateingsync); เธรด t2 = เธรดใหม่ (accateingsync); t1.start (); t2.start (); t1.join (); t2.join (); System.out.println (i); - ในการแก้ปัญหานี้โดยพื้นฐานเราต้องตรวจสอบให้แน่ใจว่าหลายเธรดจะต้องซิงโครไนซ์อย่างสมบูรณ์เมื่อใช้งาน i กล่าวคือเมื่อเธรด A Writes I, Thread B ไม่เพียง แต่ไม่สามารถเขียนได้ แต่ยังไม่สามารถอ่านได้
1. บทบาทของคำหลักที่ซิงโครไนซ์
ฟังก์ชั่นของคำหลักที่ซิงโครไนซ์เป็นจริงเพื่อให้เกิดการซิงโครไนซ์ระหว่างเธรด งานของมันคือการล็อครหัสที่ซิงโครไนซ์เพื่อให้มีเพียงเธรดเดียวเท่านั้นที่สามารถป้อนบล็อกการซิงโครไนซ์ในแต่ละครั้งเพื่อให้มั่นใจว่าการรักษาความปลอดภัยระหว่างเธรด เช่นเดียวกับในรหัสด้านบนการดำเนินการ I ++ สามารถดำเนินการโดยเธรดอื่นในเวลาเดียวกันเท่านั้น
2. ใช้คำหลักที่ซิงโครไนซ์
ระบุวัตถุล็อค: ล็อควัตถุที่กำหนดป้อนบล็อกรหัสการซิงโครไนซ์เพื่อรับล็อคของวัตถุที่กำหนด
ทำหน้าที่โดยตรงกับวิธีการอินสแตนซ์: เทียบเท่ากับการล็อคอินสแตนซ์ปัจจุบัน การป้อนบล็อกรหัสแบบซิงโครนัสคุณจะต้องได้รับการล็อคของอินสแตนซ์ปัจจุบัน (ต้องการว่าเมื่อสร้างเธรดคุณต้องใช้อินสแตนซ์ที่เรียกใช้งานเดียวกันได้)
ทำหน้าที่โดยตรงกับวิธีการคงที่: เทียบเท่ากับการล็อคคลาสปัจจุบัน ก่อนที่จะเข้าสู่บล็อกรหัสแบบซิงโครนัสคุณต้องได้รับการล็อคของคลาสปัจจุบัน
2.1 ระบุวัตถุที่จะล็อค
รหัสต่อไปนี้ใช้ซิงโครไนซ์กับวัตถุที่กำหนด มีบันทึกที่นี่ว่าวัตถุที่กำหนดจะต้องคงที่มิฉะนั้นเราจะไม่แบ่งปันวัตถุซึ่งกันและกันทุกครั้งที่เราใหม่เธรดดังนั้นความหมายของการล็อคจะไม่มีอยู่อีกต่อไป
Class Public Accateingsync ใช้งาน Runnable {Object final Static Object = New Object (); int คงที่ i = 0; โมฆะสาธารณะเพิ่ม () {i ++; } @Override โมฆะสาธารณะเรียกใช้ () {สำหรับ (int j = 0; j <1000000; j ++) {ซิงโครไนซ์ (วัตถุ) {เพิ่ม (); }}} โมฆะคงที่สาธารณะหลัก (สตริง [] args) พ่น InterruptedException {เธรด t1 = เธรดใหม่ (ใหม่ accateingsync ()); เธรด t2 = เธรดใหม่ (ใหม่ accateingsync ()); t1.start (); t2.start (); t1.join (); t2.join (); System.out.println (i); }} 2.2 ดำเนินการโดยตรงกับวิธีการอินสแตนซ์
คำหลักที่ซิงโครไนซ์ทำหน้าที่ในวิธีการอินสแตนซ์นั่นคือก่อนที่จะป้อนวิธีการเพิ่ม () เธรดจะต้องได้รับการล็อคของอินสแตนซ์ปัจจุบัน สิ่งนี้ต้องการให้เราใช้อินสแตนซ์ออบเจ็กต์ที่รันได้เดียวกันเมื่อสร้างอินสแตนซ์เธรด มิฉะนั้นล็อคของเธรดไม่ได้อยู่ในอินสแตนซ์เดียวกันดังนั้นจึงไม่มีวิธีพูดคุยเกี่ยวกับปัญหาการล็อค/การซิงโครไนซ์
Class Public Accateingsync ใช้งาน Runnable {Static Int i = 0; โมฆะที่ซิงโครไนซ์สาธารณะเพิ่มขึ้น () {i ++; } @Override โมฆะสาธารณะเรียกใช้ () {สำหรับ (int j = 0; j <1000000; j ++) {เพิ่ม (); }} โมฆะคงที่สาธารณะหลัก (สตริง [] args) พ่น InterruptedException {accateingsync accateingsync = accateingsync () ใหม่ (); เธรด t1 = เธรดใหม่ (accateingsync); เธรด t2 = เธรดใหม่ (accateingsync); t1.start (); t2.start (); t1.join (); t2.join (); System.out.println (i); - โปรดให้ความสนใจกับสามบรรทัดแรกของวิธีหลักเพื่อแสดงการใช้คำหลักที่ถูกต้องในวิธีการอินสแตนซ์
2.3 ทำหน้าที่โดยตรงกับวิธีการคงที่
ในการใช้คำหลักที่ซิงโครไนซ์กับวิธีการคงที่ไม่จำเป็นต้องใช้สองเธรดเพื่อชี้ไปที่วิธีการเรียกใช้แบบเดียวกันกับในตัวอย่างข้างต้น เนื่องจากบล็อกเมธอดจำเป็นต้องขอล็อคของคลาสปัจจุบันไม่ใช่อินสแตนซ์ปัจจุบันเธรดยังสามารถซิงโครไนซ์ได้อย่างถูกต้อง
Class Public Accateingsync ใช้งาน Runnable {Static Int i = 0; โมฆะแบบซิงโครไนซ์แบบคงที่สาธารณะเพิ่มขึ้น () {i ++; } @Override โมฆะสาธารณะเรียกใช้ () {สำหรับ (int j = 0; j <1000000; j ++) {เพิ่ม (); }} โมฆะคงที่สาธารณะหลัก (สตริง [] args) พ่น InterruptedException {เธรด t1 = เธรดใหม่ (ใหม่ accateingsync ()); เธรด t2 = เธรดใหม่ (ใหม่ accateingsync ()); t1.start (); t2.start (); t1.join (); t2.join (); System.out.println (i); -3. ล็อคไม่ถูกต้อง
จากตัวอย่างข้างต้นเรารู้ว่าหากเราต้องการแอปพลิเคชันตัวนับเพื่อให้แน่ใจว่ามีความถูกต้องของข้อมูลเราจะต้องล็อคตัวนับตามธรรมชาติดังนั้นเราอาจเขียนรหัสต่อไปนี้:
ชั้นเรียนสาธารณะ Badlockoninteger ใช้งาน Runnable {Integer แบบคงที่ i = 0; @Override โมฆะสาธารณะ Run () {สำหรับ (int j = 0; j <1000000; j ++) {ซิงโครไนซ์ (i) {i ++; }}} โมฆะคงที่สาธารณะหลัก (สตริง [] args) พ่น InterruptedException {badlockoninteger badlockoninteger = ใหม่ badlockoninteger (); เธรด t1 = เธรดใหม่ (badlockoninteger); เธรด t2 = เธรดใหม่ (badlockoninteger); t1.start (); t2.start (); t1.join (); t2.join (); System.out.println (i); -เมื่อเราเรียกใช้รหัสด้านบนเราจะพบว่าผลลัพธ์ที่ฉันมีขนาดเล็กมาก ซึ่งหมายความว่าเธรดไม่ปลอดภัย
เพื่ออธิบายปัญหานี้เราต้องเริ่มต้นด้วยจำนวนเต็ม: ใน Java จำนวนเต็มเป็นวัตถุที่ไม่เปลี่ยนแปลง เช่นเดียวกับสตริงเมื่อสร้างวัตถุแล้วจะไม่สามารถแก้ไขได้ หากคุณมีจำนวนเต็ม = 1 มันจะเป็น 1 เสมอถ้าคุณต้องการวัตถุนี้ = 2 คุณสามารถสร้างจำนวนเต็มใหม่ได้เท่านั้น หลังจากแต่ละ I ++ มันเทียบเท่ากับการเรียกใช้วิธีการของจำนวนเต็ม ลองดูที่ซอร์สโค้ดของวิธีการของจำนวนเต็มของวิธีการ:
ค่าคงที่ค่าคงที่สาธารณะของสาธารณะ (int i) {ถ้า (i> = integercache.low && i <= integercache.high) ส่งคืน integercache.cache [i + (-integercache.low)]; ส่งคืนจำนวนเต็มใหม่ (i);} Integer.valueof () เป็นวิธีการของโรงงานซึ่งมีแนวโน้มที่จะส่งคืนวัตถุจำนวนเต็มใหม่และคัดลอกค่าไปยัง i;
ดังนั้นเราจึงรู้เหตุผลของปัญหา เนื่องจากระหว่างหลายเธรดเนื่องจาก I ++ มาหลังจากฉันฉันชี้ไปที่วัตถุใหม่เธรดอาจโหลดอินสแตนซ์ของวัตถุที่แตกต่างกันทุกครั้งที่ล็อค การแก้ปัญหานั้นง่ายมาก คุณสามารถแก้ปัญหาได้โดยใช้หนึ่งในสามวิธีการซิงโครไนซ์ด้านบน