ตัวแปรใน Java แบ่งออกเป็นสองประเภท: ตัวแปรท้องถิ่นและตัวแปรคลาส ตัวแปรเฉพาะที่หมายถึงตัวแปรที่กำหนดภายในวิธีการ เช่น ตัวแปรที่กำหนดในวิธีการเรียกใช้ สำหรับตัวแปรเหล่านี้ จะไม่มีปัญหาในการแบ่งใช้ระหว่างเธรด ดังนั้นจึงไม่จำเป็นต้องมีการซิงโครไนซ์ข้อมูล ตัวแปรคลาสคือตัวแปรที่กำหนดในคลาส และขอบเขตของตัวแปรคือทั้งคลาส ตัวแปรดังกล่าวสามารถใช้ร่วมกันได้หลายเธรด ดังนั้นเราจึงจำเป็นต้องทำการซิงโครไนซ์ข้อมูลกับตัวแปรดังกล่าว
การซิงโครไนซ์ข้อมูลหมายความว่ามีเพียงเธรดเดียวเท่านั้นที่สามารถเข้าถึงตัวแปรคลาสที่ซิงโครไนซ์ได้ในเวลาเดียวกัน หลังจากที่เธรดปัจจุบันเข้าถึงตัวแปรเหล่านี้แล้ว เธรดอื่นจะสามารถเข้าถึงตัวแปรเหล่านั้นต่อไปได้ การเข้าถึงที่กล่าวถึงในที่นี้หมายถึงการเข้าถึงด้วยการดำเนินการเขียน หากเธรดทั้งหมดที่เข้าถึงตัวแปรคลาสเป็นการดำเนินการอ่าน โดยทั่วไปไม่จำเป็นต้องมีการซิงโครไนซ์ข้อมูล แล้วจะเกิดอะไรขึ้นหากไม่ทำการซิงโครไนซ์ข้อมูลกับตัวแปรคลาสที่แชร์? ก่อนอื่นเรามาดูกันว่าเกิดอะไรขึ้นกับรหัสต่อไปนี้:
คัดลอกรหัสรหัสดังต่อไปนี้:
การทดสอบบรรจุภัณฑ์
MyThread คลาสสาธารณะขยายเธรด
-
int สาธารณะคงที่ n = 0;
การรันโมฆะสาธารณะ ()
-
อินท์ ม. = n;
ผลผลิต();
ม++;
n = ม.;
-
โมฆะคงที่สาธารณะ main (String [] args) พ่นข้อยกเว้น
-
MyThread myThread = MyThread ใหม่ ();
เธรดเธรด[] = เธรดใหม่[100];
สำหรับ (int i = 0; i < threads.length; i++)
เธรด [i] = เธรดใหม่ (myThread);
สำหรับ (int i = 0; i < threads.length; i++)
กระทู้[i].เริ่มต้น();
สำหรับ (int i = 0; i < threads.length; i++)
กระทู้[i].เข้าร่วม();
System.out.println("n = " + MyThread.n);
-
-
ผลลัพธ์ที่เป็นไปได้ของการรันโค้ดข้างต้นมีดังนี้:
คัดลอกรหัสรหัสดังต่อไปนี้:
n=59
ผู้อ่านหลายคนอาจประหลาดใจเมื่อเห็นผลลัพธ์นี้ เห็นได้ชัดว่าโปรแกรมนี้เริ่ม 100 เธรด จากนั้นแต่ละเธรดจะเพิ่มตัวแปรคงที่ n คูณ 1 สุดท้าย ใช้วิธีรวมเพื่อทำให้เธรดทั้งหมด 100 รายการรัน จากนั้นจึงส่งออกค่า n โดยปกติผลลัพธ์ควรเป็น n = 100 แต่ผลออกมาไม่ถึง 100
อันที่จริงแล้ว ผู้ร้ายของผลลัพธ์นี้คือ "ข้อมูลสกปรก" ที่เรามักพูดถึง คำสั่ง Yield() ในเมธอด run เป็นตัวเริ่มต้นของ "dirty data" (โดยไม่ต้องเพิ่ม Yield คำสั่งก็อาจสร้าง "dirty data" ได้เช่นกัน แต่จะไม่ชัดเจนนัก เพียงเปลี่ยน 100 เป็นตัวเลขที่มากขึ้นเท่านั้นที่จะ มันมักจะเกิดขึ้น การสร้าง "ข้อมูลสกปรก" การเรียกผลตอบแทนในตัวอย่างนี้คือการขยายผลกระทบของ "ข้อมูลสกปรก" ฟังก์ชันของวิธีการ Yield คือการหยุดเธรดชั่วคราว กล่าวคือ ทำให้เธรดที่เรียกวิธีการ Yield เป็นการสิ้นเปลืองทรัพยากรของ CPU เป็นการชั่วคราว ทำให้ CPU มีโอกาสที่จะดำเนินการกับเธรดอื่น เพื่อแสดงให้เห็นว่าโปรแกรมนี้สร้าง "ข้อมูลสกปรก" อย่างไร สมมติว่ามีเพียงสองเธรดเท่านั้นที่ถูกสร้างขึ้น: thread1 และ thread2 เนื่องจากเมธอด start ของ thread1 ถูกเรียกก่อน โดยทั่วไปเมธอด run ของ thread1 จะถูกรันก่อน เมื่อวิธี run ของ thread1 วิ่งไปที่บรรทัดแรก (int m = n;) ค่าของ n จะถูกกำหนดให้กับ m เมื่อดำเนินการวิธีผลผลิตของบรรทัดที่สอง thread1 จะหยุดดำเนินการชั่วคราว เมื่อ thread1 ถูกหยุดชั่วคราว thread2 จะเริ่มทำงานหลังจากได้รับทรัพยากร CPU (thread2 อยู่ในสถานะพร้อมก่อนหน้านี้) เมื่อ thread2 ดำเนินการบรรทัดแรก (int When m = n;) เนื่องจาก n ยังคงเป็น 0 เมื่อ thread1 ถูกดำเนินการเพื่อให้ได้ผล ค่าที่ได้รับจาก m ใน thread2 จึงเป็น 0 เช่นกัน สิ่งนี้ทำให้ค่า m ของ thread1 และ thread2 ทั้งคู่ได้รับ 0 หลังจากที่ดำเนินการตามวิธี Yield แล้ว พวกเขาทั้งหมดจะเริ่มต้นจาก 0 และเพิ่ม 1 ดังนั้น ไม่ว่าใครจะเป็นผู้ดำเนินการก่อน ค่าสุดท้ายของ n คือ 1 แต่ n นี้ถูกกำหนดโดย thread1 และ thread2 ตามลำดับ บางคนอาจถามว่าถ้ามีแค่ n++ แล้ว "ข้อมูลสกปรก" จะถูกสร้างขึ้นหรือไม่? คำตอบคือใช่ ดังนั้น n++ จึงเป็นเพียงคำสั่ง ดังนั้นจะส่งมอบ CPU ให้กับเธรดอื่นระหว่างการดำเนินการได้อย่างไร อันที่จริง นี่เป็นเพียงปรากฏการณ์ผิวเผิน หลังจากที่คอมไพเลอร์ Java รวบรวม n++ เป็นภาษากลาง (หรือที่เรียกว่า bytecode) แล้ว มันก็ไม่ใช่ภาษา เรามาดูกันว่าภาษากลาง Java ใดที่โค้ด Java ต่อไปนี้จะถูกคอมไพล์เป็นภาษาใด
คัดลอกรหัสรหัสดังต่อไปนี้:
การรันโมฆะสาธารณะ ()
-
n++;
-
รวบรวมรหัสภาษากลาง
คัดลอกรหัสรหัสดังต่อไปนี้:
การรันโมฆะสาธารณะ ()
-
aload_0
ซ้ำซ้อน
เก็ตฟิลด์
ไอคอนst_1
ไอแอด
พัตฟิลด์
กลับ
-
คุณจะเห็นว่ามีเพียงคำสั่ง n++ ในเมธอด run แต่หลังจากการคอมไพล์แล้ว จะมีคำสั่งภาษากลางอยู่ 7 รายการ เราไม่จำเป็นต้องรู้ว่าฟังก์ชันของข้อความเหล่านี้คืออะไร เพียงแค่ดูที่ข้อความในบรรทัด 005, 007 และ 008 บรรทัด 005 คือ getfield ตามความหมายภาษาอังกฤษ เรารู้ว่าเราต้องการได้รับค่าที่แน่นอน เนื่องจากมี n เพียงตัวเดียว จึงไม่มีข้อสงสัยว่าเราต้องการค่า n ไม่ยากเลยที่จะเดาว่า iadd ในบรรทัด 007 จะต้องบวก 1 เข้ากับค่า n ที่ได้รับ ฉันคิดว่าคุณอาจเดาความหมายของ putfield ในบรรทัด 008 ได้ โดยมีหน้าที่รับผิดชอบในการอัปเดต n หลังจากเพิ่ม 1 กลับไปยังตัวแปรคลาส n เมื่อพูดถึงเรื่องนี้ คุณอาจยังมีข้อสงสัยอยู่ เมื่อดำเนินการ n++ แค่เพิ่ม n ด้วย 1 ก็เพียงพอแล้ว เหตุใดจึงเกิดปัญหามากมาย ที่จริงแล้วสิ่งนี้เกี่ยวข้องกับปัญหาโมเดลหน่วยความจำ Java
โมเดลหน่วยความจำของ Java แบ่งออกเป็นพื้นที่จัดเก็บข้อมูลหลักและพื้นที่จัดเก็บข้อมูลการทำงาน พื้นที่เก็บข้อมูลหลักจัดเก็บอินสแตนซ์ทั้งหมดใน Java กล่าวคือ หลังจากที่เราใช้ new เพื่อสร้างอ็อบเจ็กต์ อ็อบเจ็กต์และวิธีการภายใน ตัวแปร ฯลฯ จะถูกบันทึกไว้ในพื้นที่นี้ และ n ในคลาส MyThread จะถูกบันทึกไว้ในพื้นที่นี้ ที่เก็บข้อมูลหลักสามารถแชร์โดยทุกเธรด พื้นที่เก็บข้อมูลการทำงานคือเธรดสแต็กที่เราพูดถึงก่อนหน้านี้ ในพื้นที่นี้ ตัวแปรที่กำหนดในวิธีการเรียกใช้และวิธีที่เรียกโดยวิธีการเรียกใช้จะถูกจัดเก็บ นั่นคือ ตัวแปรวิธีการ เมื่อเธรดต้องการแก้ไขตัวแปรในพื้นที่เก็บข้อมูลหลัก เธรดจะไม่แก้ไขตัวแปรเหล่านี้โดยตรง แต่คัดลอกไปยังพื้นที่เก็บข้อมูลการทำงานของเธรดปัจจุบัน หลังจากการแก้ไขเสร็จสิ้น ค่าตัวแปรจะถูกเขียนทับด้วยค่าที่สอดคล้องกัน ค่าในพื้นที่เก็บข้อมูลหลัก ค่าตัวแปร
หลังจากทำความเข้าใจโมเดลหน่วยความจำของ Java แล้ว ก็ไม่ใช่เรื่องยากที่จะเข้าใจว่าเหตุใด n++ จึงไม่ใช่การดำเนินการแบบอะตอมมิก จะต้องผ่านขั้นตอนการคัดลอก เพิ่ม 1 และเขียนทับ กระบวนการนี้คล้ายกับกระบวนการจำลองในคลาส MyThread ดังที่คุณสามารถจินตนาการได้ หาก thread1 ถูกขัดจังหวะด้วยเหตุผลบางประการเมื่อดำเนินการ getfield สถานการณ์ที่คล้ายกับผลการดำเนินการของคลาส MyThread ก็จะเกิดขึ้น เพื่อแก้ไขปัญหานี้อย่างสมบูรณ์ เราต้องใช้วิธีการบางอย่างในการซิงโครไนซ์ n นั่นคือ มีเพียงเธรดเดียวเท่านั้นที่สามารถดำเนินการ n ในเวลาเดียวกัน ซึ่งเรียกอีกอย่างว่าการดำเนินการแบบอะตอมมิกบน n