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