รู้เบื้องต้นเกี่ยวกับโมเดลหน่วยความจำ 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 เรากรองกรณีที่เหลือออกโดยใช้การอ่านแบบระเหย ความถูกต้องได้รับการยืนยันจากข้อเท็จจริงที่ว่าร้านค้าที่เปลี่ยนแปลงได้เกิดขึ้นก่อนการอ่านที่เปลี่ยนแปลงได้ และการดำเนินการทั้งหมดที่เกิดขึ้นในตัวสร้างจะมองเห็นได้แก่ผู้ใดก็ตามที่อ่านค่าของฟิลด์