คำแนะนำเกี่ยวกับ Volatile Keyword ใน Java

1. ภาพรวม

ในกรณีที่ไม่มีการซิงโครไนซ์ที่จำเป็นคอมไพลเลอร์รันไทม์หรือตัวประมวลผลอาจใช้การเพิ่มประสิทธิภาพทุกประเภท แม้ว่าการเพิ่มประสิทธิภาพเหล่านี้จะเป็นประโยชน์เกือบตลอดเวลา แต่บางครั้งก็อาจทำให้เกิดปัญหาเล็กน้อยได้

การแคชและการจัดลำดับใหม่เป็นหนึ่งในการเพิ่มประสิทธิภาพที่อาจทำให้เราประหลาดใจในบริบทที่เกิดขึ้นพร้อมกัน Java และ JVM มีหลายวิธีในการควบคุมลำดับหน่วยความจำและคำสำคัญที่ลบเลือนก็เป็นหนึ่งในนั้น

ในบทความนี้เราจะมุ่งเน้นแนวคิดพื้นฐาน แต่มักจะเข้าใจผิดในภาษา Java - The ระเหยคำหลัก ก่อนอื่นเราจะเริ่มต้นด้วยพื้นหลังเล็กน้อยเกี่ยวกับการทำงานของสถาปัตยกรรมคอมพิวเตอร์พื้นฐานจากนั้นเราจะทำความคุ้นเคยกับลำดับหน่วยความจำใน Java

2. สถาปัตยกรรมมัลติโปรเซสเซอร์ที่ใช้ร่วมกัน

โปรเซสเซอร์มีหน้าที่ดำเนินการตามคำสั่งของโปรแกรม ดังนั้นพวกเขาจำเป็นต้องดึงทั้งคำสั่งโปรแกรมและข้อมูลที่ต้องการจาก RAM

เนื่องจากซีพียูสามารถดำเนินการตามคำสั่งจำนวนมากต่อวินาทีการดึงข้อมูลจาก RAM จึงไม่เหมาะสำหรับพวกเขา เพื่อปรับปรุงสถานการณ์นี้โปรเซสเซอร์กำลังใช้กลอุบายเช่นการดำเนินการนอกคำสั่งการทำนายสาขาการดำเนินการเก็งกำไรและแน่นอนการแคช

นี่คือที่มาของลำดับชั้นของหน่วยความจำต่อไปนี้:

เนื่องจากคอร์ที่แตกต่างกันดำเนินการคำสั่งมากขึ้นและจัดการกับข้อมูลมากขึ้นพวกเขาก็เติมแคชด้วยข้อมูลและคำแนะนำที่เกี่ยวข้อง นี้จะช่วยเพิ่มประสิทธิภาพโดยรวมที่ค่าใช้จ่ายของการแนะนำความท้าทายการเชื่อมโยงกันแคช

พูดง่ายๆก็คือเราควรคิดถึงสิ่งที่เกิดขึ้นเมื่อเธรดหนึ่งอัปเดตค่าแคช

3. เมื่อใดควรใช้สารระเหย

เพื่อที่จะขยายเพิ่มเติมเกี่ยวกับการเชื่อมโยงกันของแคชให้ยืมตัวอย่างหนึ่งจากหนังสือ Java Concurrency in Practice:

public class TaskRunner { private static int number; private static boolean ready; private static class Reader extends Thread { @Override public void run() { while (!ready) { Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { new Reader().start(); number = 42; ready = true; } }

TaskRunnerระดับรักษาสองตัวแปรที่เรียบง่าย ในวิธีการหลักจะสร้างเธรดอื่นที่หมุนบนตัวแปรพร้อมตราบเท่าที่เป็นเท็จ เมื่อตัวแปรกลายเป็นจริงเธรดก็จะพิมพ์ตัวแปรตัวเลข

หลายคนอาจคาดหวังว่าโปรแกรมนี้จะพิมพ์ 42 หลังจากล่าช้าไปชั่วครู่ อย่างไรก็ตามในความเป็นจริงความล่าช้าอาจนานกว่านี้มาก มันอาจค้างตลอดไปหรือแม้แต่พิมพ์ศูนย์!

สาเหตุของความผิดปกติเหล่านี้คือขาดการมองเห็นหน่วยความจำที่เหมาะสมและการจัดลำดับ มาประเมินรายละเอียดเพิ่มเติมกัน

3.1. การมองเห็นหน่วยความจำ

ในตัวอย่างง่ายๆนี้เรามีเธรดแอปพลิเคชันสองเธรด: เธรดหลักและเธรดตัวอ่าน ลองนึกภาพสถานการณ์ที่ระบบปฏิบัติการกำหนดเวลาเธรดเหล่านั้นบนแกน CPU สองแกนที่แตกต่างกันโดยที่:

  • เธรดหลักมีสำเนาของตัวแปรพร้อมและตัวเลขในแคชหลัก
  • เธรดผู้อ่านจะลงท้ายด้วยสำเนาด้วย
  • เธรดหลักจะอัพเดตค่าที่แคชไว้

สำหรับโปรเซสเซอร์ที่ทันสมัยส่วนใหญ่คำขอเขียนจะไม่ถูกนำไปใช้ทันทีหลังจากที่ออก ในความเป็นจริงการประมวลผลมีแนวโน้มที่จะคิวเขียนผู้ที่อยู่ในบัฟเฟอร์เขียนพิเศษ หลังจากนั้นสักครู่พวกเขาจะนำการเขียนเหล่านั้นไปใช้กับหน่วยความจำหลักทั้งหมดในครั้งเดียว

จากที่กล่าวมาทั้งหมดเมื่อเธรดหลักอัปเดตจำนวนและตัวแปรพร้อมไม่มีการรับประกันว่าเธรดผู้อ่านจะเห็นอะไร กล่าวอีกนัยหนึ่งเธรดผู้อ่านอาจเห็นค่าที่อัปเดตทันทีหรือมีความล่าช้าบ้างหรือไม่เลย!

การมองเห็นหน่วยความจำนี้อาจทำให้เกิดปัญหาความเป็นอยู่ในโปรแกรมที่อาศัยการมองเห็น

3.2. การจัดลำดับใหม่

ที่จะทำให้เรื่องยิ่งแย่ลงกระทู้ผู้อ่านอาจจะเห็นผู้ที่อยู่ในการเขียนคำสั่งอื่นใดนอกเหนือจากการสั่งซื้อโปรแกรมจริง ตัวอย่างเช่นเนื่องจากเราอัปเดตตัวแปรตัวเลขเป็นครั้งแรก:

public static void main(String[] args) { new Reader().start(); number = 42; ready = true; }

เราอาจคาดหวังว่าเธรดผู้อ่านจะพิมพ์ 42 อย่างไรก็ตามเป็นไปได้ที่จะเห็นค่าศูนย์เป็นค่าที่พิมพ์จริง!

การจัดลำดับใหม่เป็นเทคนิคการเพิ่มประสิทธิภาพสำหรับการปรับปรุงประสิทธิภาพ ที่น่าสนใจคือส่วนประกอบต่าง ๆ อาจใช้การเพิ่มประสิทธิภาพนี้:

  • โปรเซสเซอร์อาจล้างบัฟเฟอร์การเขียนในลำดับอื่นที่ไม่ใช่ลำดับโปรแกรม
  • โปรเซสเซอร์อาจใช้เทคนิคการดำเนินการนอกคำสั่ง
  • คอมไพเลอร์ JIT อาจปรับให้เหมาะสมโดยการจัดลำดับใหม่

3.3. ลำดับหน่วยความจำระเหย

เพื่อให้แน่ใจว่าการอัปเดตตัวแปรแพร่กระจายไปยังเธรดอื่น ๆ ได้อย่างคาดเดาได้เราควรใช้ตัวปรับความผันผวนกับตัวแปรเหล่านั้น:

public class TaskRunner { private volatile static int number; private volatile static boolean ready; // same as before }

ด้วยวิธีนี้เราสื่อสารกับรันไทม์และตัวประมวลผลเพื่อไม่เรียงลำดับคำสั่งใด ๆ ที่เกี่ยวข้องกับตัวแปรระเหย นอกจากนี้หน่วยประมวลผลเข้าใจว่าควรล้างการอัปเดตตัวแปรเหล่านี้ทันที

4. การซิงโครไนซ์แบบระเหยและเธรด

สำหรับแอปพลิเคชันมัลติเธรดเราจำเป็นต้องตรวจสอบให้แน่ใจว่ามีกฎสองสามข้อสำหรับพฤติกรรมที่สอดคล้องกัน:

  • การยกเว้นร่วมกัน - เธรดเดียวเท่านั้นที่ดำเนินการส่วนสำคัญในแต่ละครั้ง
  • การมองเห็น - การเปลี่ยนแปลงที่ทำโดยเธรดเดียวกับข้อมูลที่แชร์จะมองเห็นได้สำหรับเธรดอื่น ๆ เพื่อรักษาความสอดคล้องของข้อมูล

วิธีการและบล็อกที่ซิงโครไนซ์ให้คุณสมบัติทั้งสองอย่างข้างต้นโดยคิดค่าใช้จ่ายตามประสิทธิภาพของแอปพลิเคชัน

ความผันผวนค่อนข้างเป็นคำหลักที่มีประโยชน์เพราะมันสามารถช่วยให้แน่ใจว่าการด้านการแสดงผลของการเปลี่ยนแปลงข้อมูลโดยไม่ต้องของหลักสูตรให้ยกเว้นร่วมกัน ดังนั้นจึงมีประโยชน์ในสถานที่ที่เราตกลงกับหลายเธรดที่ดำเนินการบล็อกโค้ดควบคู่กันไป แต่เราจำเป็นต้องตรวจสอบคุณสมบัติการมองเห็น

5. เกิดขึ้นก่อนสั่งซื้อ

เอฟเฟกต์การมองเห็นหน่วยความจำของตัวแปรระเหยขยายไปไกลกว่าตัวแปรที่ระเหยได้เอง

เพื่อให้เรื่องเป็นรูปธรรมมากขึ้นสมมติว่าเธรด A เขียนถึงตัวแปรระเหยจากนั้นเธรด B อ่านตัวแปรระเหยเดียวกัน ในกรณีเช่นนี้ค่าที่ A มองเห็นได้ก่อนที่จะเขียนตัวแปรระเหยจะปรากฏแก่ B หลังจากอ่านตัวแปรระเหย :

เทคนิคการพูดการเขียนการใด ๆระเหยข้อมูลที่เกิดขึ้นทุกครั้งก่อนการอ่านที่ตามมาของสาขาเดียวกัน นี่คือกฎตัวแปรระเหยของ Java Memory Model (JMM)

5.1. Piggybacking

เนื่องจากความแข็งแกร่งของการเรียงลำดับหน่วยความจำที่เกิดขึ้นก่อนบางครั้งเราสามารถย้อนกลับไปที่คุณสมบัติการมองเห็นของตัวแปรระเหยอื่นได้ ตัวอย่างเช่นในตัวอย่างเฉพาะของเราเราเพียงแค่ต้องทำเครื่องหมายตัวแปรพร้อมเป็นระเหย :

public class TaskRunner { private static int number; // not volatile private volatile static boolean ready; // same as before }

สิ่งใด ๆ ก่อนที่จะเขียนtrueกับตัวแปรreadyจะปรากฏให้เห็นทุกสิ่งหลังจากอ่านตัวแปรready ดังนั้นตัวแปรตัวเลข piggybacks บนการมองเห็นหน่วยความจำที่บังคับใช้โดยตัวแปรready ใส่เพียง,แม้ว่ามันจะไม่ได้เป็นสารระเหยตัวแปรก็จะจัดแสดงนิทรรศการระเหยพฤติกรรม

ด้วยการใช้ความหมายเหล่านี้เราสามารถกำหนดตัวแปรเพียงไม่กี่ตัวในคลาสของเราว่ามีความผันผวนและเพิ่มประสิทธิภาพการรับประกันการมองเห็น

6. บทสรุป

ในการกวดวิชานี้เราได้สำรวจเพิ่มเติมเกี่ยวกับความผันผวนของคำหลักและความสามารถของตนรวมทั้งการปรับปรุงที่เกิดขึ้นกับมันเริ่มต้นด้วย Java 5

เช่นเคยตัวอย่างโค้ดสามารถพบได้ใน GitHub