สิ่งสำคัญคือต้องเข้าใจว่ามีสองด้านเพื่อความปลอดภัยของเธรด
- การควบคุมการปฏิบัติงานและ
- การมองเห็นหน่วยความจำ
สิ่งแรกที่เกี่ยวข้องกับการควบคุมเมื่อโค้ดประมวลผล (รวมถึงลำดับที่คำสั่งถูกเรียกใช้) และสามารถประมวลผลพร้อมกันได้หรือไม่และสิ่งที่สองเกี่ยวข้องกับเมื่อเอฟเฟกต์ในหน่วยความจำของสิ่งที่ได้ทำไปแล้ว เนื่องจาก CPU แต่ละตัวมีแคชหลายระดับระหว่างมันและหน่วยความจำหลักเธรดที่รันบน CPU หรือแกนที่ต่างกันสามารถเห็น "หน่วยความจำ" แตกต่างกันในช่วงเวลาใดเวลาหนึ่งเนื่องจากเธรดได้รับอนุญาตให้รับและทำงานบนสำเนาส่วนตัวของหน่วยความจำหลัก
การใช้synchronized
ป้องกันเธรดอื่น ๆ จากการได้รับจอภาพ (หรือล็อค) สำหรับวัตถุเดียวกันจึงป้องกันการบล็อกรหัสทั้งหมดได้รับการป้องกันโดยการซิงโครไนซ์บนวัตถุเดียวกันจากการดำเนินการพร้อมกัน การประสานยังสร้าง "เกิดขึ้นมาก่อน" อุปสรรคหน่วยความจำหน่วยความจำที่ก่อให้เกิดข้อ จำกัด ในการมองเห็นดังกล่าวว่าทำอะไรขึ้นมาถึงจุดรุ่นด้ายบางล็อคปรากฏด้ายอีกภายหลังการซื้อล็อคเดียวกันจะเกิดขึ้นก่อนที่จะได้มาล็อค ในทางปฏิบัติเกี่ยวกับฮาร์ดแวร์ปัจจุบันสิ่งนี้มักทำให้เกิดการล้างแคชของ CPU เมื่อได้รับจอภาพและเขียนไปยังหน่วยความจำหลักเมื่อมันถูกปล่อยออกมาซึ่งทั้งสองอย่างนั้นมีราคาค่อนข้างแพง
ใช้volatile
บนมืออื่น ๆ ที่กองกำลังเข้าถึงทั้งหมด (อ่านหรือเขียน) ตัวแปรระเหยที่จะเกิดขึ้นกับหน่วยความจำได้อย่างมีประสิทธิภาพการรักษาตัวแปรระเหยออกจากแคช CPU สิ่งนี้มีประโยชน์สำหรับการกระทำบางอย่างที่ต้องการเพียงแค่การมองเห็นตัวแปรให้ถูกต้องและลำดับการเข้าถึงไม่สำคัญ การใช้volatile
การเปลี่ยนแปลงการรักษาlong
และdouble
ยังต้องมีการเข้าถึงเพื่อให้พวกเขาเป็นอะตอม; บนฮาร์ดแวร์ (เก่า) บางตัวอาจต้องใช้การล็อกแม้ว่าจะไม่ได้ใช้กับฮาร์ดแวร์ 64 บิตที่ทันสมัย ภายใต้โมเดลหน่วยความจำใหม่ (JSR-133) สำหรับ Java 5+ ความหมายของการระเหยได้รับความเข้มแข็งจนเกือบจะแข็งแกร่งเท่ากับซิงโครไนซ์สำหรับการมองเห็นหน่วยความจำและการสั่งซื้อคำสั่ง (ดูhttp://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#volatile) สำหรับจุดประสงค์ในการมองเห็นการเข้าถึงแต่ละเขตข้อมูลจะทำหน้าที่เสมือนการซิงโครไนซ์ครึ่งหนึ่ง
ภายใต้โมเดลหน่วยความจำใหม่มันยังคงเป็นจริงที่ไม่สามารถเรียงลำดับตัวแปรที่เปลี่ยนแปลงได้ซึ่งกันและกัน ความแตกต่างคือตอนนี้มันไม่ง่ายที่จะจัดลำดับฟิลด์เข้าถึงรอบตัวพวกเขาอีกต่อไป การเขียนลงในฟิลด์ระเหยมีผลต่อหน่วยความจำเช่นเดียวกับการเปิดตัวจอภาพและการอ่านจากฟิลด์ระเหยมีผลหน่วยความจำเช่นเดียวกับจอภาพที่ได้รับ ผลเพราะหน่วยความจำแบบใหม่ที่เข้มงวดข้อ จำกัด ในการจัดเรียงใหม่ของสนามระเหยเข้าถึงด้วยการเข้าถึงข้อมูลอื่น ๆ ระเหยหรือไม่สิ่งที่มองเห็นจะด้ายA
เมื่อมันเขียนไปยังฟิลด์ระเหยf
กลายเป็นมองเห็นด้ายเมื่อมันอ่านB
f
- JSR 133 (โมเดลหน่วยความจำ Java) คำถามที่พบบ่อย
ดังนั้นตอนนี้อุปสรรคหน่วยความจำทั้งสองรูปแบบ (ภายใต้ JMM ปัจจุบัน) ทำให้เกิดอุปสรรคในการสั่งซื้อคำสั่งใหม่ซึ่งป้องกันไม่ให้คอมไพเลอร์หรือรันไทม์จากคำสั่งซื้อใหม่อีกครั้งข้ามสิ่งกีดขวาง ใน JMM เก่าความผันผวนไม่ได้ป้องกันการสั่งซื้อซ้ำ สิ่งนี้อาจมีความสำคัญเนื่องจากนอกจากข้อ จำกัด ของหน่วยความจำแล้วมีข้อ จำกัด เพียงอย่างเดียวคือ สำหรับเธรดใด ๆผลกระทบสุทธิของรหัสจะเหมือนกับที่มันเป็นหากคำสั่งถูกดำเนินการตามลำดับที่ปรากฏใน แหล่ง
การใช้งาน volatile อย่างหนึ่งสำหรับวัตถุที่ใช้ร่วมกัน แต่ไม่เปลี่ยนรูปนั้นถูกสร้างขึ้นใหม่ในทันทีโดยมีเธรดอื่น ๆ อีกมากมายที่อ้างอิงถึงวัตถุ ณ จุดใดจุดหนึ่งในวงจรการทำงาน จำเป็นต้องมีเธรดอื่น ๆ เพื่อเริ่มใช้วัตถุที่สร้างขึ้นใหม่เมื่อมีการเผยแพร่ แต่ไม่ต้องการค่าใช้จ่ายเพิ่มเติมของการซิงโครไนซ์เต็มรูปแบบและเป็นข้อขัดแย้งของผู้ดูแลและการล้างแคช
// Declaration
public class SharedLocation {
static public SomeObject someObject=new SomeObject(); // default object
}
// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
// someObject will be internally consistent for xxx(), a subsequent
// call to yyy() might be inconsistent with xxx() if the object was
// replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published
// Using code
private String getError() {
SomeObject myCopy=SharedLocation.someObject; // gets current copy
...
int cod=myCopy.getErrorCode();
String txt=myCopy.getErrorText();
return (cod+" - "+txt);
}
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.
พูดถึงคำถามอ่าน - อัปเดต - เขียนของคุณโดยเฉพาะ พิจารณารหัสที่ไม่ปลอดภัยต่อไปนี้:
public void updateCounter() {
if(counter==1000) { counter=0; }
else { counter++; }
}
ตอนนี้ด้วยเมธอด updateCounter () ที่ไม่ซิงโครไนซ์สองเธรดอาจป้อนพร้อมกัน ในการเรียงสับเปลี่ยนมากมายของสิ่งที่อาจเกิดขึ้นได้สิ่งหนึ่งคือ thread-1 ทำการทดสอบ counter == 1000 และพบว่าเป็นจริงและถูกระงับแล้ว จากนั้นเธรด -2 จะทำการทดสอบเดียวกันและเห็นว่าเป็นจริงและถูกระงับไว้ จากนั้นเธรด -1 จะดำเนินการต่อและตั้งค่าตัวนับเป็น 0 จากนั้นเธรด -2 จะกลับมาทำงานและตั้งค่าตัวนับเป็น 0 อีกครั้งเนื่องจากไม่ได้รับการอัปเดตจากเธรด -1 สิ่งนี้สามารถเกิดขึ้นได้แม้ว่าการสลับเธรดจะไม่เกิดขึ้นตามที่ฉันได้อธิบาย แต่เพียงเพราะสำเนาแคชที่แตกต่างกันสองตัวของตัวนับมีอยู่ในแกนประมวลผล CPU ที่ต่างกันสองชุดและเธรดแต่ละตัวรันบนแกนแยกกัน สำหรับเรื่องนั้นหนึ่งกระทู้อาจมีตัวนับที่หนึ่งค่าและอื่น ๆ อาจมีตัวนับที่แตกต่างกันอย่างสิ้นเชิงมูลค่าเพียงเพราะแคช
สิ่งสำคัญในตัวอย่างนี้คือตัวนับตัวแปรถูกอ่านจากหน่วยความจำหลักไปยังแคชอัพเดตในแคชและเขียนกลับไปที่หน่วยความจำหลักในบางจุดที่ไม่ทราบแน่ชัดในภายหลังเมื่อมีสิ่งกีดขวางหน่วยความจำเกิดขึ้นหรือเมื่อต้องการหน่วยความจำแคชสำหรับอย่างอื่น การทำให้ตัวนับvolatile
ไม่เพียงพอสำหรับความปลอดภัยของเธรดของรหัสนี้เนื่องจากการทดสอบสำหรับสูงสุดและการมอบหมายเป็นการดำเนินการที่ไม่ต่อเนื่องรวมถึงการเพิ่มขึ้นซึ่งเป็นชุดของread+increment+write
คำแนะนำของเครื่องที่ไม่ใช่อะตอมมิกเช่น:
MOV EAX,counter
INC EAX
MOV counter,EAX
ตัวแปรระเหยมีประโยชน์เฉพาะเมื่อการดำเนินการทั้งหมดที่ดำเนินการกับพวกเขาคือ "atomic" เช่นตัวอย่างของฉันที่การอ้างอิงไปยังวัตถุที่เกิดขึ้นอย่างสมบูรณ์เป็นเพียงอ่านหรือเขียนเท่านั้น (และโดยทั่วไปแล้วมันเขียนจากจุดเดียวเท่านั้น) อีกตัวอย่างหนึ่งคือการอ้างอิงอาร์เรย์แบบระเหยได้ซึ่งสนับสนุนรายการ copy-on-write หากอาร์เรย์นั้นถูกอ่านโดยการคัดลอกโลคัลของการอ้างอิงในครั้งแรกเท่านั้น