รู้เบื้องต้นเกี่ยวกับโมเดลหน่วยความจำ Java
Java Memory Model (JMM)อธิบายลักษณะการทำงานของเธรดในสภาพแวดล้อมรันไทม์ของ Java โมเดลหน่วยความจำเป็นส่วนหนึ่งของความหมายของภาษา Java และอธิบายถึงสิ่งที่โปรแกรมเมอร์สามารถและไม่ควรคาดหวังเมื่อพัฒนาซอฟต์แวร์ที่ไม่ใช่สำหรับเครื่อง Java เฉพาะ แต่สำหรับ Java โดยรวม
โมเดลหน่วยความจำ Java ดั้งเดิม (ซึ่งโดยเฉพาะอย่างยิ่งหมายถึง "หน่วยความจำ percolocal") ที่พัฒนาขึ้นในปี 1995 ถือว่าล้มเหลว การปรับให้เหมาะสมหลายอย่างไม่สามารถทำได้โดยไม่สูญเสียการรับประกันความปลอดภัยของโค้ด โดยเฉพาะอย่างยิ่งมีหลายตัวเลือกในการเขียน "single" แบบมัลติเธรด:
- การเข้าถึงซิงเกิลตันทุกครั้ง (แม้ว่าวัตถุจะถูกสร้างขึ้นเมื่อนานมาแล้วและไม่มีอะไรเปลี่ยนแปลงได้) จะทำให้เกิดการล็อกระหว่างเธรด
- หรือภายใต้สถานการณ์บางอย่าง ระบบจะออก loner ที่ยังไม่เสร็จ
- หรือภายใต้สถานการณ์บางอย่าง ระบบจะสร้างผู้โดดเดี่ยวสองคน
- หรือการออกแบบจะขึ้นอยู่กับลักษณะการทำงานของเครื่องนั้นๆ
ดังนั้นกลไกหน่วยความจำจึงได้รับการออกแบบใหม่ ในปี 2548 ด้วยการเปิดตัว Java 5 มีการนำเสนอวิธีการใหม่ ซึ่งได้รับการปรับปรุงเพิ่มเติมด้วยการเปิดตัว Java 14
โมเดลใหม่เป็นไปตามกฎสามข้อ:
กฎข้อที่ 1 : โปรแกรมแบบเธรดเดียวทำงานตามลำดับหลอก ซึ่งหมายความว่า: ในความเป็นจริง โปรเซสเซอร์สามารถดำเนินการหลายอย่างต่อสัญญาณนาฬิกา ในขณะเดียวกันก็เปลี่ยนลำดับ อย่างไรก็ตาม การพึ่งพาข้อมูลทั้งหมดยังคงอยู่ ดังนั้นลักษณะการทำงานจึงไม่แตกต่างจากการทำงานตามลำดับ
กฎข้อที่ 2 : ไม่มีค่าใดๆ การอ่านตัวแปรใดๆ (ยกเว้น long และ double ที่ไม่ลบเลือน ซึ่งกฎนี้อาจไม่ถือ) จะส่งคืนค่าดีฟอลต์ (ศูนย์) หรือบางอย่างที่เขียนโดยคำสั่งอื่น
และกฎข้อที่ 3 : กิจกรรมที่เหลือจะดำเนินการตามลำดับ หากเชื่อมต่อกันด้วยความสัมพันธ์ของคำสั่งบางส่วนที่เข้มงวด "ดำเนินการก่อน" ( เกิดขึ้นก่อน )
เกิดขึ้นก่อน
เลส ลีแลมพอร์ตได้แนวคิดเรื่องHappens before นี่คือความสัมพันธ์ของคำสั่งบางส่วนที่เข้มงวดซึ่งนำมาใช้ระหว่างคำสั่งอะตอมมิก (++ และ -- ไม่ใช่อะตอมมิก) และไม่ได้หมายความว่า "ทางกายภาพมาก่อน"
มันบอกว่าทีมที่สองจะ "รู้" การเปลี่ยนแปลงที่เกิดขึ้นโดยทีมแรก
ตัวอย่างเช่น มีการดำเนินการหนึ่งก่อนการดำเนินการอื่นสำหรับการดำเนินการดังกล่าว:
การซิงโครไนซ์และมอนิเตอร์:
- การจับภาพจอภาพ ( วิธี การล็อคการซิงโครไนซ์การเริ่มต้น) และอะไรก็ตามที่เกิดขึ้นในเธรดเดียวกันหลังจากนั้น
- การกลับมาของมอนิเตอร์ (วิธีการปลดล็อคสิ้นสุดการซิงโครไนซ์) และอะไรก็ตามที่เกิดขึ้นในเธรดเดียวกันก่อนหน้านี้
- คืนจอภาพแล้วจับภาพด้วยเธรดอื่น
การเขียนและการอ่าน:
- เขียนไปยังตัวแปรใด ๆ แล้วอ่านในสตรีมเดียวกัน
- ทุกอย่างในเธรดเดียวกันก่อนที่จะเขียนถึงตัวแปรที่เปลี่ยนแปลงได้และการเขียนเอง อ่านระเหยและทุกอย่างในหัวข้อเดียวกันหลังจากนั้น
- เขียนถึงตัวแปรที่เปลี่ยนแปลงได้ แล้วอ่านอีกครั้ง การเขียนแบบระเหยโต้ตอบกับหน่วยความจำในลักษณะเดียวกับการส่งคืนจอภาพ ในขณะที่การอ่านเป็นเหมือนการจับภาพ ปรากฎว่าหากเธรดหนึ่งเขียนถึงตัวแปรที่เปลี่ยนแปลงได้ และเธรดที่สองพบมัน ทุกอย่างที่อยู่ก่อนหน้าการเขียนจะถูกดำเนินการก่อนทุกอย่างที่อยู่หลังการอ่าน ดูภาพ
การบำรุงรักษาวัตถุ:
- การเริ่มต้นแบบสแตติกและการดำเนินการใด ๆ กับอินสแตนซ์ของวัตถุ
- เขียนไปยังฟิลด์สุดท้ายในตัวสร้างและทุกอย่างหลังจากตัวสร้าง ข้อยกเว้น ความสัมพันธ์แบบhaps-beforeจะไม่เชื่อมต่อกับกฎอื่นแบบสกรรมกริยา ดังนั้นจึงสามารถทำให้เกิดการแข่งขันระหว่างเธรดได้
- ทำงานกับออบเจกต์และfinalize( )
บริการสตรีม:
- การเริ่มเธรดและโค้ดใดๆ ในเธรด
- ตัวแปร Zeroing ที่เกี่ยวข้องกับเธรดและโค้ดใดๆ ในเธรด
- รหัสในเธรดและเข้าร่วม () ; รหัสในเธรดและisAlive() == false
- ขัดจังหวะ ()เธรดและตรวจสอบว่าหยุดทำงานแล้ว
เกิดขึ้นก่อนความแตกต่างในการทำงาน
การปล่อยจอภาพที่จะเกิดขึ้นก่อนที่จะได้รับจอภาพเดียวกัน เป็นที่น่าสังเกตว่านี่คือการเปิดตัว ไม่ใช่ทางออก นั่นคือคุณไม่ต้องกังวลเกี่ยวกับความปลอดภัยเมื่อใช้การรอ
มาดูกันว่าความรู้นี้จะช่วยให้เราแก้ไขตัวอย่างของเราได้อย่างไร ในกรณีนี้ทุกอย่างง่ายมาก: เพียงลบการตรวจสอบภายนอกและปล่อยให้การซิงโครไนซ์เหมือนเดิม ตอนนี้รับประกันว่าเธรดที่สองจะเห็นการเปลี่ยนแปลงทั้งหมด เนื่องจากเธรดจะได้รับจอภาพหลังจากเธรดอื่นปล่อยเท่านั้น และเนื่องจากเขาจะไม่ปล่อยมันจนกว่าทุกอย่างจะเริ่มต้น เราจะเห็นการเปลี่ยนแปลงทั้งหมดในคราวเดียว ไม่ใช่แยกจากกัน:
public class Keeper {
private Data data = null;
public Data getData() {
synchronized(this) {
if(data == null) {
data = new Data();
}
}
return data;
}
}
การเขียนถึงตัวแปรที่ผันผวนเกิดขึ้นก่อนที่จะอ่านจากตัวแปรเดียวกัน แน่นอนว่าการเปลี่ยนแปลงที่เราทำนั้นช่วยแก้ไขข้อบกพร่อง แต่มันทำให้ใครก็ตามที่เขียนโค้ดต้นฉบับกลับไปที่เดิม นั่นคือการบล็อกทุกครั้ง คำหลักที่ผันผวนสามารถบันทึกได้ อันที่จริง ข้อความที่เป็นคำถามหมายความว่าเมื่ออ่านทุกอย่างที่ประกาศเป็นความผันผวน เราจะได้ค่าที่แท้จริงเสมอ
นอกจากนี้ อย่างที่ฉันได้กล่าวไปก่อนหน้านี้ สำหรับฟิลด์ที่ผันผวน การเขียนมักจะเป็นการดำเนินการของอะตอมเสมอ (รวมถึงแบบยาวและสองเท่า) ประเด็นสำคัญอีกประการ: หากคุณมีเอนทิตีที่เปลี่ยนแปลงได้ซึ่งมีการอ้างอิงถึงเอนทิตีอื่น (เช่น อาร์เรย์ รายการ หรือคลาสอื่นๆ) การอ้างอิงถึงเอนทิตีเท่านั้นที่จะ "ใหม่" เสมอ แต่ไม่ใช่ทุกอย่างใน มันเข้ามา
ดังนั้น กลับไปที่เครื่องล็อกแบบดับเบิ้ลล็อคของเรา เมื่อใช้ volatile คุณสามารถแก้ไขสถานการณ์เช่นนี้:
public class Keeper {
private volatile Data data = null;
public Data getData() {
if(data == null) {
synchronized(this) {
if(data == null) {
data = new Data();
}
}
}
return data;
}
}
ที่นี่เรายังมีล็อคอยู่ แต่ถ้า data == null เรากรองกรณีที่เหลือออกโดยใช้การอ่านแบบระเหย ความถูกต้องได้รับการยืนยันจากข้อเท็จจริงที่ว่าร้านค้าที่เปลี่ยนแปลงได้เกิดขึ้นก่อนการอ่านที่เปลี่ยนแปลงได้ และการดำเนินการทั้งหมดที่เกิดขึ้นในตัวสร้างจะมองเห็นได้แก่ผู้ใดก็ตามที่อ่านค่าของฟิลด์
GO TO FULL VERSION