วัสดุออนไลน์จำนวนมากอธิบายโมเดลหน่วยความจำ Java ซึ่งจะแนะนำว่ามีหน่วยความจำหลักและเธรดคนงานแต่ละคนมีหน่วยความจำในการทำงานของตัวเอง จะมีข้อมูลหนึ่งชิ้นในหน่วยความจำหลักและหนึ่งชิ้นในหน่วยความจำที่ใช้งานได้ จะมีการดำเนินการอะตอมที่หลากหลายระหว่างหน่วยความจำการทำงานและหน่วยความจำหลักเพื่อซิงโครไนซ์
ภาพต่อไปนี้มาจากบล็อกนี้
อย่างไรก็ตามเนื่องจากวิวัฒนาการอย่างต่อเนื่องของเวอร์ชัน Java รุ่นหน่วยความจำได้เปลี่ยนไปเช่นกัน บทความนี้พูดถึงคุณสมบัติบางอย่างของโมเดลหน่วยความจำ Java เท่านั้น ไม่ว่าจะเป็นโมเดลหน่วยความจำใหม่หรือโมเดลหน่วยความจำเก่ามันจะดูชัดเจนขึ้นหลังจากทำความเข้าใจกับคุณสมบัติเหล่านี้
1. atomicity
Atomicity หมายความว่าการดำเนินการไม่หยุดยั้ง แม้ว่าจะมีการดำเนินการหลายเธรดเข้าด้วยกันเมื่อการดำเนินการเริ่มต้นขึ้นมันจะไม่ถูกรบกวนโดยเธรดอื่น ๆ
เป็นที่เชื่อกันโดยทั่วไปว่าคำแนะนำของ CPU เป็นการดำเนินการอะตอม แต่รหัสที่เราเขียนไม่จำเป็นต้องดำเนินการอะตอม
ตัวอย่างเช่น i ++ การดำเนินการนี้ไม่ใช่การดำเนินการอะตอมโดยทั่วไปจะแบ่งออกเป็น 3 การดำเนินการอ่าน i, ดำเนินการ +1 และกำหนดค่าให้กับ i
สมมติว่ามีสองเธรด เมื่อเธรดแรกอ่าน i = 1 การดำเนินการ +1 ยังไม่ได้ดำเนินการและเปลี่ยนเป็นเธรดที่สอง ในเวลานี้เธรดที่สองยังอ่าน i = 1 จากนั้นสองเธรดจะดำเนินการ +1 ในภายหลังจากนั้นกำหนดค่ากลับมาฉันไม่ใช่ 3 แต่ 2. เห็นได้ชัดว่ามีความไม่สอดคล้องกันในข้อมูล
ตัวอย่างเช่นการอ่านค่าความยาว 64 บิตของ JVM 32 บิตไม่ใช่การดำเนินการอะตอม แน่นอน JVM 32 บิตอ่านจำนวนเต็ม 32 บิตเป็นการดำเนินการอะตอม
2. คำสั่งซื้อ
ในระหว่างการเกิดขึ้นพร้อมกันการดำเนินการของโปรแกรมอาจไม่เป็นระเบียบ
เมื่อคอมพิวเตอร์ดำเนินการรหัสไม่จำเป็นต้องดำเนินการตามลำดับของโปรแกรม
คลาส orderExample {int a = 0; ธงบูลีน = เท็จ; นักเขียนโมฆะสาธารณะ () {a = 1; ธง = จริง; } public void reader () {if (flag) {int i = a +1; - ตัวอย่างเช่นในรหัสข้างต้นมีสองวิธีที่เรียกโดยสองเธรดตามลำดับ ตามสามัญสำนึกเธรดการเขียนควรดำเนินการ A = 1 ก่อนจากนั้นเรียกใช้ FLAG = TRUE เมื่อเธรดอ่านกำลังอ่านฉัน = 2;
แต่เนื่องจาก a = 1 และ flag = true ไม่มีความสัมพันธ์เชิงตรรกะ ดังนั้นจึงเป็นไปได้ที่จะย้อนกลับลำดับของการดำเนินการและเป็นไปได้ที่จะเรียกใช้ Flag = TRUE ก่อนแล้ว A = 1 ในเวลานี้เมื่อ Flag = TRUE สลับไปที่เธรดอ่าน ในเวลานี้ A = 1 ยังไม่ถูกดำเนินการแล้วเธรดการอ่านจะเป็น i = 1
แน่นอนว่านี่ไม่ใช่แน่นอน เป็นไปได้ว่าจะมีการสั่งซื้อและอาจไม่เกิดขึ้น
แล้วทำไมถึงมีระเบียบ? สิ่งนี้เริ่มต้นด้วยคำสั่ง CPU หลังจากรวบรวมรหัสใน Java ในที่สุดก็จะถูกแปลงเป็นรหัสประกอบ
การดำเนินการของคำสั่งสามารถแบ่งออกเป็นหลายขั้นตอน สมมติว่าคำสั่ง CPU แบ่งออกเป็นขั้นตอนต่อไปนี้
สมมติว่ามีสองคำแนะนำที่นี่
โดยทั่วไปแล้วเราจะคิดว่าคำแนะนำจะดำเนินการตามลำดับก่อนดำเนินการคำสั่ง 1 ก่อนจากนั้นดำเนินการคำสั่ง 2 โดยสมมติว่าแต่ละขั้นตอนต้องใช้ระยะเวลา 1 ซีพียูจากนั้นดำเนินการคำสั่งทั้งสองนี้ต้องใช้ระยะเวลา CPU 10 ช่วงเวลาซึ่งไม่มีประสิทธิภาพเกินกว่าที่จะทำเช่นนั้น ในความเป็นจริงคำแนะนำจะถูกดำเนินการแบบขนาน แน่นอนเมื่อคำสั่งแรกดำเนินการหากคำสั่งที่สองไม่สามารถทำได้หากเนื่องจากคำสั่งลงทะเบียนและสิ่งที่ไม่สามารถถูกครอบครองได้ในเวลาเดียวกัน ดังที่แสดงในรูปด้านบนคำแนะนำทั้งสองจะถูกดำเนินการแบบขนานในลักษณะที่ค่อนข้างเซ เมื่อคำสั่ง 1 ดำเนินการ ID คำสั่ง 2 จะดำเนินการ IF ด้วยวิธีนี้มีการดำเนินการสองคำแนะนำในช่วงเวลาเพียง 6 ช่วงเวลาซีพียูซึ่งค่อนข้างมีประสิทธิภาพ
ตามแนวคิดนี้ลองมาดูกันว่าคำสั่ง A = B+C ดำเนินการอย่างไร
ดังที่แสดงในรูปจะมีการดำเนินการว่าง (x) ในระหว่างการดำเนินการเพิ่มเนื่องจากเมื่อคุณต้องการเพิ่ม b และ c เมื่อการดำเนินการ X ของ Add ในรูป C ไม่ได้อ่านจากหน่วยความจำ (C อ่านจากหน่วยความจำเท่านั้นเมื่อการดำเนินการ MEM เสร็จสมบูรณ์ ฮาร์ดแวร์ดังนั้นจึงไม่จำเป็นต้องรอให้ WB ดำเนินการก่อนที่จะทำการเพิ่ม) ดังนั้นจะมีเวลาว่าง (x) ในการดำเนินการเพิ่ม ในการดำเนินการ SW เนื่องจากคำสั่ง EX ไม่สามารถดำเนินการพร้อมกันกับคำสั่ง Add Ex ได้จะมีเวลาว่าง (x)
ถัดไปขอยกตัวอย่างที่ซับซ้อนขึ้นเล็กน้อย
a = b+c
d = ef
คำแนะนำที่เกี่ยวข้องมีดังนี้
เหตุผลคล้ายกับข้างต้นดังนั้นฉันจะไม่วิเคราะห์ที่นี่ เราพบว่ามี X จำนวนมากที่นี่และมีรอบเวลาจำนวนมากเสียไปและประสิทธิภาพก็ได้รับผลกระทบเช่นกัน มีวิธีลดจำนวน XS หรือไม่?
เราหวังว่าจะใช้การดำเนินการบางอย่างเพื่อเติมเวลาว่างของ X เนื่องจาก Add มีการพึ่งพาข้อมูลกับคำแนะนำข้างต้นและเราหวังว่าจะใช้คำแนะนำบางอย่างโดยไม่ต้องพึ่งพาข้อมูลเพื่อเติมเวลาว่างที่เกิดจากการพึ่งพาข้อมูล
เราเปลี่ยนลำดับของคำแนะนำ
หลังจากเปลี่ยนลำดับคำแนะนำ X จะถูกกำจัด ระยะเวลาการดำเนินการโดยรวมก็ลดลงเช่นกัน
การสั่งซื้อการสั่งซื้อใหม่สามารถทำให้ท่อราบรื่นขึ้น
แน่นอนว่าหลักการของการสอนการจัดเรียงใหม่คือมันไม่สามารถทำลายความหมายของโปรแกรมอนุกรม ตัวอย่างเช่น a = 1, b = a+1 คำแนะนำดังกล่าวจะไม่ถูกจัดเรียงใหม่เนื่องจากผลลัพธ์แบบอนุกรมของการจัดเรียงใหม่นั้นแตกต่างจากคำสั่งดั้งเดิม
การจัดเรียงคำแนะนำเป็นเพียงวิธีการเพิ่มประสิทธิภาพคอมไพเลอร์หรือซีพียูและการเพิ่มประสิทธิภาพนี้ทำให้เกิดปัญหากับโปรแกรมในตอนต้นของบทนี้
วิธีแก้ปัญหา? ใช้คำหลักที่ผันผวนซีรีย์ที่ตามมานี้จะได้รับการแนะนำ
3. ทัศนวิสัย
การมองเห็นหมายถึงว่าเธรดอื่น ๆ สามารถทราบการปรับเปลี่ยนได้ทันทีเมื่อเธรดปรับเปลี่ยนค่าของตัวแปรที่ใช้ร่วมกันหรือไม่
ปัญหาการมองเห็นอาจเกิดขึ้นในลิงค์ต่างๆ ตัวอย่างเช่นการสั่งซื้อการสั่งซื้อใหม่ที่กล่าวถึงจะทำให้เกิดปัญหาการมองเห็นและนอกจากนี้การเพิ่มประสิทธิภาพของคอมไพเลอร์หรือการเพิ่มประสิทธิภาพของฮาร์ดแวร์บางอย่างจะทำให้เกิดปัญหาการมองเห็น
ตัวอย่างเช่นเธรดปรับค่าที่ใช้ร่วมกันให้เหมาะสมในหน่วยความจำในขณะที่เธรดอื่นปรับค่าที่ใช้ร่วมกันให้เหมาะสมกับแคช เมื่อแก้ไขค่าในหน่วยความจำค่าแคชไม่ทราบการปรับเปลี่ยน
ตัวอย่างเช่นการเพิ่มประสิทธิภาพฮาร์ดแวร์บางอย่างเมื่อโปรแกรมเขียนหลายครั้งไปยังที่อยู่เดียวกันมันจะคิดว่ามันไม่จำเป็นและเก็บการเขียนครั้งสุดท้ายเท่านั้นดังนั้นข้อมูลที่เขียนก่อนจะมองไม่เห็นในเธรดอื่น ๆ
ในระยะสั้นปัญหาส่วนใหญ่ที่มีการมองเห็นเกิดจากการเพิ่มประสิทธิภาพ
ถัดไปดูปัญหาทัศนวิสัยที่เกิดขึ้นจากระดับเครื่องเสมือนของ Java
ปัญหามาจากบล็อก
แพ็คเกจ edu.hushi.jvm; /** * * @author -10 * */การมองเห็นระดับสาธารณะขยายขยายเธรด {Private Boolean Stop; โมฆะสาธารณะเรียกใช้ () {int i = 0; ในขณะที่ (! หยุด) {i ++; } system.out.println ("จบลูป, i =" + i); } โมฆะสาธารณะหยุด () {stop = true; } บูลีนสาธารณะ getStop () {return stop; } โมฆะคงที่สาธารณะหลัก (สตริง [] args) โยนข้อยกเว้น {visibletest v = new VisiabilityTest (); V.Start (); Thread.sleep (1,000); V.Stopit (); Thread.sleep (2000); System.out.println ("เสร็จสิ้นหลัก"); System.out.println (V.getStop ()); - รหัสนั้นง่ายมาก เธรด V รักษา I ++ ในขณะที่ลูปจนกระทั่งเธรดหลักเรียกวิธีการหยุดการเปลี่ยนค่าของตัวแปรหยุดในเธรด V เพื่อหยุดลูป
ปัญหาเกิดขึ้นเมื่อรหัสง่ายดูเหมือนจะทำงาน โปรแกรมนี้สามารถหยุดเธรดจากการดำเนินการปรับตัวด้วยตนเองในโหมดไคลเอนต์ แต่ในโหมดเซิร์ฟเวอร์มันจะเป็นลูปที่ไม่มีที่สิ้นสุดก่อน (การเพิ่มประสิทธิภาพ JVM เพิ่มเติมในโหมดเซิร์ฟเวอร์)
ระบบ 64 บิตส่วนใหญ่เป็นโหมดเซิร์ฟเวอร์และทำงานในโหมดเซิร์ฟเวอร์:
จบหลัก
จริง
เฉพาะสองประโยคนี้เท่านั้นที่จะพิมพ์ แต่จะไม่พิมพ์ลูปเสร็จสิ้น แต่คุณสามารถพบว่าค่าหยุดเป็นจริงแล้ว
ผู้เขียนบล็อกนี้ใช้เครื่องมือในการกู้คืนโปรแกรมไปยังรหัสประกอบ
มีเพียงบางส่วนของรหัสประกอบเท่านั้นที่ถูกดักจับที่นี่และส่วนสีแดงเป็นส่วนวนซ้ำ จะเห็นได้อย่างชัดเจนว่ามีเพียง 0x0193bf9d คือการตรวจสอบหยุดในขณะที่ส่วนสีแดงไม่ใช้ค่าหยุดดังนั้นจึงทำการวนรอบที่ไม่มีที่สิ้นสุด
นี่คือผลลัพธ์ของการเพิ่มประสิทธิภาพ JVM จะหลีกเลี่ยงได้อย่างไร? เช่นคำสั่งการจัดลำดับใหม่ให้ใช้คำหลักที่ผันผวน
หากมีการเพิ่มความผันผวนให้กู้คืนไปยังรหัสประกอบและคุณจะพบว่าแต่ละลูปจะได้รับค่าหยุด
ถัดไปมาดูตัวอย่างบางส่วนใน "ข้อกำหนดภาษา Java"
รูปด้านบนแสดงให้เห็นว่าการสั่งซื้อใหม่จะนำไปสู่ผลลัพธ์ที่แตกต่างกัน
เหตุผลที่ R5 = R2 ถูกสร้างขึ้นในรูปด้านบนคือ R2 = R1.x, R5 = R1.x และมันจะปรับให้เหมาะสมกับ R5 = R2 โดยตรงในเวลาคอมไพล์ ในท้ายที่สุดผลลัพธ์จะแตกต่างกัน
4. เกิดขึ้นก่อน
5. แนวคิดเรื่องความปลอดภัยของด้าย
มันหมายถึงความจริงที่ว่าเมื่อมีการเรียกฟังก์ชั่นหรือไลบรารีฟังก์ชันบางอย่างในสภาพแวดล้อมแบบมัลติเธรดมันสามารถประมวลผลตัวแปรท้องถิ่นของแต่ละเธรดได้อย่างถูกต้องและเปิดใช้งานฟังก์ชันโปรแกรมให้เสร็จสมบูรณ์อย่างถูกต้อง
ตัวอย่างเช่นตัวอย่าง i ++ ที่กล่าวถึงในตอนต้น
สิ่งนี้จะนำไปสู่ความไม่ปลอดภัยของเธรด
สำหรับรายละเอียดเกี่ยวกับความปลอดภัยของเธรดโปรดดูบล็อกนี้ที่ฉันเขียนไว้ก่อนหน้าหรือทำตามซีรี่ส์ที่ตามมาและคุณจะพูดคุยเกี่ยวกับเนื้อหาที่เกี่ยวข้อง