โมเดลหน่วยความจำ Java ที่เรียกว่า JMM เป็นการรับประกันแบบครบวงจรสำหรับชุดแพลตฟอร์มเครื่องเสมือน Java ไปยังแพลตฟอร์มเฉพาะที่ไม่เกี่ยวข้องสำหรับการมองเห็นหน่วยความจำและไม่ว่าจะมีการจัดลำดับใหม่ในสภาพแวดล้อมแบบมัลติเธรดที่จัดทำโดยนักพัฒนา (อาจมีความคลุมเครือในแง่ของคำและการกระจายหน่วยความจำของ Java Runtime ซึ่งหมายถึงพื้นที่หน่วยความจำเช่นฮีปพื้นที่วิธีการสแต็กเธรด ฯลฯ )
มีหลายรูปแบบของการเขียนโปรแกรมพร้อมกัน นอกเหนือจาก CSP (กระบวนการสื่อสารตามลำดับ) นักแสดงและรุ่นอื่น ๆ ที่คุ้นเคยที่สุดควรเป็นโมเดลหน่วยความจำที่ใช้ร่วมกันตามเธรดและล็อค ในการเขียนโปรแกรมแบบมัลติเธรดจะต้องให้ความสนใจกับ:
・ อะตอม ・ การมองเห็น ・ reorder
Atomicity เกี่ยวข้องกับว่าเธรดอื่น ๆ สามารถเห็นสถานะกลางหรือรบกวนเมื่อเธรดดำเนินการคอมโพสิต โดยทั่วไปแล้วมันเป็นปัญหาของ I ++ สองเธรดดำเนินการ ++ ในหน่วยความจำกองที่ใช้ร่วมกันในเวลาเดียวกัน การดำเนินการของการดำเนินการ ++ ใน JVM, Runtime และ CPU อาจเป็นการดำเนินการคอมโพสิต ตัวอย่างเช่นจากมุมมองของคำแนะนำ JVM มันคือการอ่านค่าของ i จากหน่วยความจำฮีปไปยังตัวถูกดำเนินการเพิ่มหนึ่งและเขียนกลับไปที่หน่วยความจำฮีป 1 ในระหว่างการดำเนินการเหล่านี้หากไม่มีการซิงโครไนซ์ที่ถูกต้องเธรดอื่น ๆ ก็สามารถดำเนินการได้ในเวลาเดียวกันซึ่งอาจนำไปสู่การสูญเสียข้อมูลและปัญหาอื่น ๆ ปัญหาเกี่ยวกับอะตอมทั่วไปหรือที่เรียกว่าสภาพการแข่งขันจะถูกตัดสินตามผลลัพธ์ความล้มเหลวที่อาจเกิดขึ้นเช่นการอ่าน-แก้ไข-เขียน การมองเห็นและการจัดระเบียบปัญหาใหม่ทั้งเกิดจากการเพิ่มประสิทธิภาพระบบ
เนื่องจากความเร็วในการดำเนินการของ CPU และความเร็วในการเข้าถึงของหน่วยความจำนั้นไม่ตรงกันอย่างจริงจังเพื่อเพิ่มประสิทธิภาพประสิทธิภาพตามหลักการการโลคัลไลเซชั่นเช่นสถานที่เวลาและพื้นที่เชิงพื้นที่ CPU ได้เพิ่มแคชหลายชั้นระหว่างหน่วยความจำ เมื่อจำเป็นต้องดึงข้อมูล CPU จะไปที่แคชก่อนเพื่อค้นหาว่ามีแคชที่เกี่ยวข้องหรือไม่ ถ้ามีอยู่มันจะถูกส่งกลับโดยตรง หากไม่มีอยู่มันจะถูกนำไปใช้ในหน่วยความจำและบันทึกไว้ในแคช ตอนนี้โปรเซสเซอร์แบบมัลติคอร์มากขึ้นได้กลายเป็นมาตรฐานโปรเซสเซอร์แต่ละตัวมีแคชของตัวเองซึ่งเกี่ยวข้องกับปัญหาของความสอดคล้องของแคช ซีพียูมีแบบจำลองที่สอดคล้องกันของจุดแข็งและจุดอ่อนที่แตกต่างกัน ความมั่นคงที่แข็งแกร่งที่สุดคือความปลอดภัยสูงสุดและยังสอดคล้องกับโหมดการคิดตามลำดับของเรา อย่างไรก็ตามในแง่ของประสิทธิภาพจะมีค่าใช้จ่ายมากมายเนื่องจากความต้องการการสื่อสารที่ประสานงานกันระหว่างซีพียูที่แตกต่างกัน
แผนภาพโครงสร้างแคช CPU ทั่วไปมีดังนี้
วงจรการเรียนการสอนของ CPU มักจะใช้คำสั่งการแยกวิเคราะห์คำแนะนำในการอ่านข้อมูลการดำเนินการคำแนะนำและการเขียนข้อมูลกลับไปที่การลงทะเบียนหรือหน่วยความจำ เมื่อดำเนินการตามคำแนะนำในอนุกรมข้อมูลการอ่านและการจัดเก็บจะใช้เวลานานดังนั้น CPU โดยทั่วไปจะใช้ไปป์ไลน์คำสั่งเพื่อดำเนินการหลายคำสั่งในเวลาเดียวกันเพื่อปรับปรุงปริมาณงานโดยรวมเช่นเดียวกับท่อส่งข้อมูลจากโรงงาน
ความเร็วในการอ่านข้อมูลและการเขียนข้อมูลกลับไปยังหน่วยความจำไม่ได้อยู่ในลำดับขนาดเท่ากันมากกว่าการดำเนินการตามคำแนะนำดังนั้น CPU จึงใช้การลงทะเบียนและแคชเป็นแคชและบัฟเฟอร์ เมื่ออ่านข้อมูลจากหน่วยความจำมันจะอ่านบรรทัดแคช (คล้ายกับการอ่านดิสก์และอ่านบล็อก) โมดูลที่เขียนข้อมูลกลับจะทำให้คำขอจัดเก็บข้อมูลลงในบัฟเฟอร์ร้านค้าเมื่อข้อมูลเก่าไม่ได้อยู่ในแคชและยังคงดำเนินการต่อไปในขั้นตอนต่อไปของวงจรคำสั่ง หากมีอยู่ในแคชแคชจะได้รับการปรับปรุงและข้อมูลในแคชจะล้างไปยังหน่วยความจำตามนโยบายที่แน่นอน
MemoryModel ระดับสาธารณะ {จำนวน int ส่วนตัว; บูลีนส่วนตัวหยุด; โมฆะสาธารณะ initCountandStop () {count = 1; หยุด = เท็จ; } โมฆะสาธารณะ doloop () {ในขณะที่ (! หยุด) {นับ ++; }} โมฆะสาธารณะ printresult () {system.out.println (นับ); System.out.println (หยุด); -เมื่อดำเนินการรหัสด้านบนเราอาจคิดว่า count = 1 จะถูกดำเนินการก่อนหยุด = false สิ่งนี้ถูกต้องในสถานะอุดมคติที่แสดงในแผนภาพการดำเนินการ CPU ข้างต้น แต่ไม่ถูกต้องเมื่อพิจารณาการลงทะเบียนและบัฟเฟอร์แคช ตัวอย่างเช่นหยุดตัวเองอยู่ในแคช แต่การนับไม่ได้อยู่ที่นั่นดังนั้นการหยุดอาจได้รับการอัปเดตและบัฟเฟอร์การเขียนนับจะถูกรีเฟรชเป็นหน่วยความจำก่อนที่จะเขียนกลับ
นอกจากนี้ CPU และคอมไพเลอร์ (โดยปกติจะอ้างถึง JIT สำหรับ Java) อาจแก้ไขคำสั่งการดำเนินการตามคำสั่ง ตัวอย่างเช่นในรหัสข้างต้น count = 1 และ stop = false ไม่มีการพึ่งพาดังนั้น CPU และคอมไพเลอร์อาจแก้ไขลำดับของสองนี้ ในมุมมองของโปรแกรมเธรดเดี่ยวผลลัพธ์จะเหมือนกัน นี่คือ as-if-serial ที่ CPU และคอมไพเลอร์จะต้องตรวจสอบให้แน่ใจ (ไม่ว่าคำสั่งการดำเนินการจะถูกแก้ไขอย่างไรผลการดำเนินการของเธรดเดี่ยวยังคงไม่เปลี่ยนแปลง) เนื่องจากการดำเนินการโปรแกรมส่วนใหญ่เป็นเธรดเดี่ยวการเพิ่มประสิทธิภาพดังกล่าวจึงเป็นที่ยอมรับและนำการปรับปรุงประสิทธิภาพที่ยอดเยี่ยม อย่างไรก็ตามในกรณีของการทำมัลติเธรดผลลัพธ์ที่ไม่คาดคิดอาจเกิดขึ้นได้โดยไม่ต้องดำเนินการซิงโครไนซ์ที่จำเป็น ตัวอย่างเช่นหลังจากเธรด T1 ดำเนินการเมธอด InitCountandStop เธรด T2 จะดำเนินการ printresult ซึ่งอาจเป็น 0, false, 1, false หรือ 0, true หากเธรด T1 ดำเนินการ doloop () ก่อนและเธรด T2 จะดำเนินการ initCountandStop หนึ่งวินาทีดังนั้น T1 อาจกระโดดออกจากลูปหรืออาจไม่เห็นการปรับเปลี่ยนการหยุดเนื่องจากการเพิ่มประสิทธิภาพคอมไพเลอร์
เนื่องจากปัญหาต่าง ๆ ในสถานการณ์มัลติเธรดข้างต้นลำดับโปรแกรมในมัลติเธรดไม่ได้เป็นคำสั่งการดำเนินการอีกต่อไปและส่งผลให้กลไกพื้นฐาน ภาษาการเขียนโปรแกรมจำเป็นต้องให้การรับประกันแก่นักพัฒนา ในแง่ง่ายการรับประกันนี้คือเมื่อการปรับเปลี่ยนเธรดจะมองเห็นได้ในเธรดอื่น ๆ ดังนั้นภาษา Java จึงเสนอ JavamemoryModel นั่นคือโมเดลหน่วยความจำ Java ซึ่งต้องใช้การดำเนินการตามอนุสัญญาของรุ่นนี้ Java จัดเตรียมกลไกเช่นความผันผวนการซิงโครไนซ์และขั้นสุดท้ายเพื่อช่วยให้นักพัฒนามั่นใจในความถูกต้องของโปรแกรมแบบมัลติเธรดบนแพลตฟอร์มโปรเซสเซอร์ทั้งหมด
ก่อน JDK1.5 โมเดลหน่วยความจำของ Java มีปัญหาร้ายแรง ตัวอย่างเช่นในโมเดลหน่วยความจำเก่าเธรดอาจเห็นค่าเริ่มต้นของฟิลด์สุดท้ายหลังจากตัวสร้างเสร็จสมบูรณ์และการเขียนของฟิลด์ผันผวนอาจถูกจัดลำดับใหม่ด้วยการอ่านและเขียนฟิลด์ที่ไม่ลบเลือน
ดังนั้นใน JDK1.5 มีการเสนอโมเดลหน่วยความจำใหม่ผ่าน JSR133 เพื่อแก้ไขปัญหาก่อนหน้านี้
การจัดลำดับกฎใหม่
ความผันผวนและล็อคจอภาพ
| เป็นไปได้ไหมที่จะจัดลำดับใหม่ | การดำเนินการที่สอง | การดำเนินการที่สอง | การดำเนินการที่สอง |
|---|---|---|---|
| การดำเนินการครั้งแรก | การอ่าน/การเขียนธรรมดาปกติ | การอ่าน/จอภาพที่ผันผวน | ทางออกการเขียน/การตรวจสอบที่ผันผวน |
| การอ่าน/การเขียนธรรมดาปกติ | เลขที่ | ||
| Voaltile Read/Monitor Enter | เลขที่ | เลขที่ | เลขที่ |
| ทางออกการเขียน/การตรวจสอบที่ผันผวน | เลขที่ | เลขที่ |
การอ่านปกติหมายถึงอาร์เรย์ของ GetField, GetStatic และอาร์เรย์ที่ไม่ระเหยและการอ่านปกติหมายถึงอาร์เรย์ของ Putfield, Putstatic และอาร์เรย์ที่ไม่ระเหย
การอ่านและการเขียนของเขตข้อมูลที่ผันผวนคือ getfield, getstatic, putfield, putstatic, ตามลำดับ
MonitorEnter คือการเข้าสู่วิธีการซิงโครไนซ์บล็อกหรือวิธีการซิงโครไนซ์ Monitorexist หมายถึงการออกจากบล็อกการซิงโครไนซ์หรือวิธีการซิงโครไนซ์
ไม่อยู่ในตารางข้างต้นหมายถึงการดำเนินการสองครั้งที่ไม่อนุญาตให้มีการจัดลำดับใหม่ ตัวอย่างเช่น (การเขียนปกติ, การเขียนที่ผันผวน) หมายถึงการจัดลำดับใหม่ของฟิลด์ที่ไม่ระเหยและการจัดเรียงใหม่ของการเขียนของฟิลด์ผันผวนที่ตามมา เมื่อไม่มี NO หมายความว่าอนุญาตให้มีการจัดลำดับใหม่ได้ แต่ JVM จำเป็นต้องตรวจสอบความปลอดภัยขั้นต่ำ - ค่าการอ่านเป็นค่าเริ่มต้นหรือเขียนโดยเธรดอื่น ๆ (64- บิตการอ่านและเขียนยาวเป็นกรณีพิเศษเมื่อไม่มีการปรับเปลี่ยนที่แตกต่างกัน
สนามสุดท้าย
มีกฎพิเศษเพิ่มเติมอีกสองกฎสำหรับฟิลด์สุดท้าย
ทั้งการเขียนของฟิลด์สุดท้าย (ในคอนสตรัคเตอร์) หรือการเขียนการอ้างอิงของวัตถุฟิลด์สุดท้ายนั้นสามารถจัดลำดับใหม่ได้ด้วยการเขียนของวัตถุที่ถือฟิลด์สุดท้าย (นอกคอนสตรัคเตอร์) ตัวอย่างเช่นคำสั่งต่อไปนี้ไม่สามารถจัดลำดับใหม่ได้
x.finalfield = v; - SharedRef = x;
โหลดครั้งแรกของฟิลด์สุดท้ายไม่สามารถจัดเรียงใหม่ด้วยการเขียนของวัตถุที่ถือฟิลด์สุดท้าย ตัวอย่างเช่นคำสั่งต่อไปนี้ไม่อนุญาตให้มีการจัดลำดับใหม่
x = Sharedref; - i = x.finalfield
สิ่งกีดขวางหน่วยความจำ
โปรเซสเซอร์ทั้งหมดรองรับอุปสรรคหรือรั้วหน่วยความจำบางอย่างเพื่อควบคุมการมองเห็นการจัดลำดับใหม่และข้อมูลระหว่างโปรเซสเซอร์ที่แตกต่างกัน ตัวอย่างเช่นเมื่อ CPU เขียนข้อมูลกลับมามันจะใส่คำขอร้านค้าลงในบัฟเฟอร์การเขียนและรอการล้างเข้าไปในหน่วยความจำ คำขอร้านค้านี้สามารถป้องกันไม่ให้มีการจัดลำดับใหม่ด้วยคำขออื่น ๆ โดยการแทรกอุปสรรคเพื่อให้แน่ใจว่าการมองเห็นข้อมูล คุณสามารถใช้ตัวอย่างชีวิตเพื่อเปรียบเทียบอุปสรรค ตัวอย่างเช่นเมื่อใช้ลิฟต์ความลาดชันบนรถไฟใต้ดินทุกคนเข้าสู่ลิฟต์ตามลำดับ แต่บางคนจะไปรอบ ๆ จากด้านซ้ายดังนั้นคำสั่งเมื่อออกจากลิฟต์แตกต่างกัน หากบุคคลมีกระเป๋าเดินทางขนาดใหญ่ที่ถูกบล็อก (สิ่งกีดขวาง) ผู้คนที่อยู่เบื้องหลังไม่สามารถไปรอบ ๆ ได้ :) นอกจากนี้อุปสรรคที่นี่และอุปสรรคการเขียนที่ใช้ใน GC เป็นแนวคิดที่แตกต่างกัน
การจำแนกประเภทของอุปสรรคหน่วยความจำ
โปรเซสเซอร์เกือบทั้งหมดสนับสนุนคำแนะนำอุปสรรคของเมล็ดหยาบบางชนิดซึ่งมักเรียกว่ารั้ว (รั้วรั้ว) ซึ่งสามารถมั่นใจได้ว่าคำแนะนำในการโหลดและการจัดเก็บที่เริ่มต้นก่อนที่รั้วจะสามารถทำได้อย่างเคร่งครัดตามลำดับและเก็บหลังจากรั้ว โดยปกติแล้วมันจะถูกแบ่งออกเป็นอุปสรรคสี่ประเภทต่อไปนี้ตามวัตถุประสงค์ของพวกเขา
อุปสรรคโหลดโหลด
โหลด 1; โหลด; โหลด 2;
ตรวจสอบให้แน่ใจว่าโหลดข้อมูล LOAD1 ก่อนโหลด 2 และหลังโหลด
อุปสรรค Storestore
ร้านค้า 1; Storestore; ร้านค้า 2
ตรวจสอบให้แน่ใจว่าข้อมูลใน Store1 สามารถมองเห็นได้สำหรับโปรเซสเซอร์อื่น ๆ ก่อนที่จะจัดเก็บ 2 และหลัง
อุปสรรคในการโหลด
โหลด 1; Loadstore; ร้านค้า 2
ตรวจสอบให้แน่ใจว่าข้อมูลของ Load1 ถูกโหลดก่อน Store2 และหลังจากล้างข้อมูล
อุปสรรค Storeload
ร้านค้า 1; Storeload; โหลด 2
ตรวจสอบให้แน่ใจว่าข้อมูลใน Store1 สามารถมองเห็นได้ต่อหน้าโปรเซสเซอร์อื่น ๆ (เช่นการล้างไปยังหน่วยความจำ) ก่อนที่จะโหลดข้อมูลใน Load2 และหลังโหลด Storeload Barrier ป้องกันการโหลดจากการอ่านข้อมูลเก่ามากกว่าข้อมูลที่เพิ่งเขียนโดยโปรเซสเซอร์อื่น ๆ
เกือบทุกมัลติโปรเซสเซอร์ในยุคปัจจุบันต้องการ StorEload ค่าใช้จ่ายของ storeload มักจะใหญ่ที่สุดและ storeload มีผลกระทบของอุปสรรคอื่น ๆ อีกสามข้อดังนั้น storeload สามารถใช้เป็นอุปสรรคทั่วไป (แต่สูงกว่า)
ดังนั้นการใช้อุปสรรคหน่วยความจำข้างต้นกฎการจัดลำดับใหม่ในตารางด้านบนสามารถนำไปใช้ได้
| ต้องการอุปสรรค | การดำเนินการที่สอง | การดำเนินการที่สอง | การดำเนินการที่สอง | การดำเนินการที่สอง |
|---|---|---|---|---|
| การดำเนินการครั้งแรก | การอ่านปกติ | งานเขียนปกติ | การอ่าน/จอภาพที่ผันผวน | ทางออกการเขียน/การตรวจสอบที่ผันผวน |
| การอ่านปกติ | โหลดสโตร์ | |||
| การอ่านปกติ | เก็บของ | |||
| Voaltile Read/Monitor Enter | โหลด | โหลดสโตร์ | โหลด | โหลดสโตร์ |
| ทางออกการเขียน/การตรวจสอบที่ผันผวน | storeLoad | เก็บของ |
เพื่อที่จะสนับสนุนกฎของฟิลด์สุดท้ายจำเป็นต้องเพิ่มอุปสรรคในการเขียนขั้นสุดท้ายไปยังรอบชิงชนะเลิศ
x.finalfield = v; Storestore; SharedRef = x;
แทรกอุปสรรคหน่วยความจำ
ตามกฎข้างต้นคุณสามารถเพิ่มอุปสรรคในการประมวลผลของฟิลด์ผันผวนและคำหลักที่ซิงโครไนซ์เพื่อให้ตรงกับกฎของโมเดลหน่วยความจำ
แทรก Storestore ก่อนที่จะมีอุปสรรคร้านค้าที่ผันผวนหลังจากที่มีการเขียนฟิลด์สุดท้ายทั้งหมด แต่แทรก Storestore ก่อนที่คอนสตรัคเตอร์จะกลับมา
ใส่สิ่งกีดขวาง storeload หลังจากร้านค้าผันผวน แทรกอุปสรรคโหลดและโหลดสโตร์หลังจากโหลดระเหยได้
การตรวจสอบ Enter และกฎการโหลดที่ผันผวนนั้นสอดคล้องกันและกฎการออกจากจอภาพและกฎการจัดเก็บที่ผันผวนนั้นสอดคล้องกัน
เกิดขึ้นก่อน
อุปสรรคหน่วยความจำต่าง ๆ ที่กล่าวถึงข้างต้นยังคงค่อนข้างซับซ้อนสำหรับนักพัฒนาดังนั้น JMM สามารถใช้ชุดของกฎของความสัมพันธ์บางส่วนของความสัมพันธ์ที่เกิดขึ้นก่อนที่จะแสดงให้เห็น เพื่อให้แน่ใจว่าเธรดที่ดำเนินการ Operation B เห็นผลลัพธ์ของการดำเนินการ A (ไม่ว่า A และ B จะถูกดำเนินการในเธรดเดียวกัน) จากนั้นความสัมพันธ์ที่เกิดขึ้นก่อนจะต้องพบระหว่าง A และ B มิฉะนั้น JVM สามารถสั่งซื้อใหม่ได้โดยพลการ
เกิดขึ้นก่อนรายการกฎ
HappendBefore กฎรวมถึง
กฎลำดับของโปรแกรม: หากการดำเนินการในโปรแกรมคือก่อนการทำงาน B การดำเนินการ A ในเธรดเดียวกันจะดำเนินการตามกฎการล็อคจอมอนิเตอร์ก่อนการดำเนินการ B: การดำเนินการล็อคบนล็อคมอนิเตอร์จะต้องดำเนินการก่อนที่จะดำเนินการล็อคบนล็อคจอภาพเดียวกัน
กฎตัวแปรผันผวน: การดำเนินการเขียนของตัวแปรผันผวนจะต้องดำเนินการกฎการเริ่มต้นเธรดก่อนการดำเนินการอ่านของตัวแปร: การเรียกไปที่เธรดเริ่มต้นในเธรดจะต้องดำเนินการกฎปลายเธรดก่อนการดำเนินการใด ๆ ในเธรด: การดำเนินการใด ๆ ในเธรดจะต้องดำเนินการอินเตอร์รัปต์ การดำเนินการ B ดำเนินการก่อนการดำเนินการ C จากนั้นดำเนินการ A จะดำเนินการก่อนการดำเนินการ C
ล็อคจอแสดงผลมีความหมายหน่วยความจำเหมือนกับล็อคจอภาพและตัวแปรอะตอมมีความหมายหน่วยความจำเหมือนกับผันผวน การได้มาและการเปิดตัวของล็อคการดำเนินการอ่านและเขียนของตัวแปรผันผวนนั้นเป็นไปตามความสัมพันธ์เต็มรูปแบบดังนั้นการเขียนความผันผวนสามารถทำได้ก่อนที่จะอ่านได้
สิ่งที่กล่าวถึงข้างต้นสามารถรวมกันได้โดยใช้หลายกฎ
ตัวอย่างเช่นหลังจากเธรด A เข้าสู่การล็อคจอภาพการดำเนินการก่อนที่จะปล่อยล็อคจอภาพจะขึ้นอยู่กับกฎลำดับของโปรแกรมและการดำเนินการรีลีสมอนิเตอร์เกิดขึ้นก่อนที่จะใช้เพื่อรับล็อคจอภาพเดียวกันในเธรด B ที่ตามมาและการดำเนินการในการดำเนินการ
สรุป
ข้างต้นเป็นคำอธิบายโดยละเอียดทั้งหมดของ JAVA Memory Model JMM ในบทความนี้ฉันหวังว่ามันจะเป็นประโยชน์กับทุกคน หากมีข้อบกพร่องใด ๆ โปรดฝากข้อความไว้เพื่อชี้ให้เห็น ขอบคุณเพื่อนที่ให้การสนับสนุนเว็บไซต์นี้!