ความแตกต่างระหว่างสารระเหยและซิงโครไนซ์ใน Java


233

ฉันสงสัยว่าความแตกต่างระหว่างการประกาศตัวแปรเป็นvolatileและการเข้าถึงตัวแปรในsynchronized(this)บล็อกใน Java หรือไม่?

ตามบทความนี้http://www.javamex.com/tutorials/synchronization_volatile.shtmlมีหลายสิ่งที่ต้องพูดและมีความแตกต่างมากมาย แต่ก็มีความคล้ายคลึงกันบ้าง

ฉันสนใจข้อมูลนี้เป็นพิเศษ:

...

  • การเข้าถึงตัวแปรที่เปลี่ยนแปลงได้ไม่เคยมีศักยภาพในการบล็อก: เราเพียง แต่ทำการอ่านหรือเขียนอย่างง่าย ๆ เท่านั้นซึ่งแตกต่างจากบล็อกที่ซิงโครไนซ์ที่เราจะไม่ยึดติดกับล็อคใด ๆ
  • เนื่องจากการเข้าถึงตัวแปรระเหยไม่เคยมีการล็อคจึงไม่เหมาะสำหรับกรณีที่เราต้องการอ่าน - อัปเดต - เขียนเป็นปฏิบัติการอะตอมมิก (เว้นแต่ว่าเราพร้อมที่จะ "พลาดการอัพเดท");

พวกเขาหมายถึงอะไรโดยread-update-write ? การเขียนนั้นเป็นการอัปเดตหรือหมายความว่าการอัปเดตนั้นเป็นการเขียนที่ขึ้นอยู่กับการอ่านด้วยหรือไม่

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

คำตอบ:


383

สิ่งสำคัญคือต้องเข้าใจว่ามีสองด้านเพื่อความปลอดภัยของเธรด

  1. การควบคุมการปฏิบัติงานและ
  2. การมองเห็นหน่วยความจำ

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

- 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 หากอาร์เรย์นั้นถูกอ่านโดยการคัดลอกโลคัลของการอ้างอิงในครั้งแรกเท่านั้น


5
ขอบคุณมาก ๆ! ตัวอย่างที่มีตัวนับนั้นเข้าใจง่าย อย่างไรก็ตามเมื่อสิ่งต่าง ๆ เกิดขึ้นจริงมันต่างออกไปเล็กน้อย
Albus Dumbledore

"ในทางปฏิบัติเกี่ยวกับฮาร์ดแวร์ปัจจุบันสิ่งนี้มักทำให้เกิดการล้างแคชของ CPU เมื่อได้รับจอภาพและเขียนไปยังหน่วยความจำหลักเมื่อมันถูกปล่อยออกมาซึ่งทั้งคู่มีราคาแพง (ค่อนข้างพูด)" . เมื่อคุณพูดแคชของ CPU จะเหมือนกับ Java Stacks ในแต่ละเธรดหรือไม่ หรือเธรดมี Heap เวอร์ชันในตัวของมันเอง? ขอโทษถ้าฉันโง่ที่นี่
NishM

1
@nishm มันไม่เหมือนกัน แต่จะรวมแคชท้องถิ่นของเธรดที่เกี่ยวข้อง .
Lawrence Dol

1
@ MarianPaździoch: การเพิ่มขึ้นหรือลดลงไม่ใช่การอ่านหรือการเขียนเป็นการอ่านและการเขียน มันเป็นการอ่านลงทะเบียนจากนั้นเพิ่มการลงทะเบียนจากนั้นเขียนกลับไปที่หน่วยความจำ การอ่านและเขียนเป็นแบบเอกเทศแต่การดำเนินการหลายอย่างไม่ได้เป็นเช่นนั้น
Lawrence Dol

2
ดังนั้นตามคำถามที่พบบ่อยไม่เพียง แต่การกระทำที่ทำตั้งแต่การได้มาล็อคจะปรากฏให้เห็นหลังจากปลดล็อค แต่การกระทำทั้งหมดที่ทำโดยเธรดนั้นจะปรากฏให้เห็น แม้แต่การกระทำที่เกิดขึ้นก่อนการเข้าล็อค
Lii

97

ระเหยเป็นตัวปรับแต่งข้อมูลในขณะที่ตรงกันปรับเปลี่ยนบล็อกรหัสและวิธีการ ดังนั้นเราสามารถระบุ accessor แบบง่าย ๆ สามแบบโดยใช้คำสำคัญสองคำเหล่านี้:

    int i1;
    int geti1() {return i1;}

    volatile int i2;
    int geti2() {return i2;}

    int i3;
    synchronized int geti3() {return i3;}

geti1()เข้าถึงค่าที่เก็บอยู่i1ในเธรดปัจจุบัน เธรดสามารถมีสำเนาของตัวแปรในเครื่องและข้อมูลไม่จำเป็นต้องเหมือนกับข้อมูลที่เก็บไว้ในเธรดอื่น ๆ โดยเฉพาะเธรดอื่นอาจมีการอัพเดตi1ในเธรดของมัน แต่ค่าในเธรดปัจจุบันอาจแตกต่างจากที่ ปรับปรุงค่า ในความเป็นจริง Java มีแนวคิดของหน่วยความจำ "หลัก" และนี่คือหน่วยความจำที่เก็บค่า "ถูกต้อง" ปัจจุบันสำหรับตัวแปร เธรดสามารถมีสำเนาของข้อมูลสำหรับตัวแปรของตนเองและสำเนาเธรดอาจแตกต่างจากหน่วยความจำ "หลัก" ดังนั้นในความเป็นจริงมันเป็นไปได้ที่หน่วยความจำ "main" จะมีค่า1สำหรับi1, สำหรับ thread1 มีค่า2สำหรับi1และthread2เพื่อให้มีค่า3สำหรับi1ถ้าthread1และthread2มีทั้ง i1 ที่อัพเดต แต่ค่าที่อัพเดตเหล่านั้นยังไม่ถูกเผยแพร่ไปยังหน่วยความจำ "main" หรือเธรดอื่น

ในทางกลับกันgeti2()เข้าถึงค่าของi2หน่วยความจำ "หลัก" ได้อย่างมีประสิทธิภาพ ตัวแปรแบบระเหยไม่ได้รับอนุญาตให้มีสำเนาของตัวแปรที่แตกต่างจากค่าที่อยู่ในหน่วยความจำ "หลัก" ในปัจจุบัน อย่างมีประสิทธิภาพตัวแปรที่ประกาศได้จะต้องมีข้อมูลที่ตรงกันในทุกเธรดดังนั้นเมื่อใดก็ตามที่คุณเข้าถึงหรืออัพเดตตัวแปรในเธรดใด ๆ เธรดอื่นทั้งหมดจะเห็นค่าเดียวกันทันที โดยทั่วไปแล้วตัวแปรที่ระเหยได้จะมีการเข้าถึงและอัปเดตค่าใช้จ่ายสูงกว่าตัวแปร "ธรรมดา" โดยทั่วไปแล้วเธรดจะได้รับอนุญาตให้มีสำเนาของข้อมูลเพื่อประสิทธิภาพที่ดีขึ้น

มีความแตกต่างสองอย่างระหว่าง volitile และการซิงโครไนซ์

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

  1. เธรดจะได้รับการล็อคบนจอภาพสำหรับวัตถุนี้
  2. หน่วยความจำของเธรดจะล้างตัวแปรทั้งหมดนั่นคือมีตัวแปรทั้งหมดที่อ่านได้อย่างมีประสิทธิภาพจากหน่วยความจำ "หลัก"
  3. บล็อกโค้ดถูกเรียกใช้งาน (ในกรณีนี้ตั้งค่าส่งคืนเป็นค่าปัจจุบันของ i3 ซึ่งอาจเพิ่งถูกรีเซ็ตจากหน่วยความจำ "main")
  4. (โดยปกติการเปลี่ยนแปลงตัวแปรใด ๆ จะถูกเขียนไปยังหน่วยความจำ "main" แต่สำหรับ geti3 () เราไม่มีการเปลี่ยนแปลง)
  5. ด้ายปล่อยล็อคบนจอภาพสำหรับวัตถุนี้

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

http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html


35
-1, Volatile ไม่ได้รับการล็อคมันใช้สถาปัตยกรรม CPU พื้นฐานเพื่อให้แน่ใจว่าการมองเห็นข้ามกระทู้ทั้งหมดหลังจากการเขียน
Michael Barker

เป็นที่น่าสังเกตว่าอาจมีบางกรณีที่อาจใช้การล็อคเพื่อรับประกัน atomicity ของการเขียน เช่นเขียนยาวบนแพลตฟอร์ม 32 บิตที่ไม่สนับสนุนสิทธิ์ความกว้างที่ขยาย Intel หลีกเลี่ยงสิ่งนี้โดยใช้การลงทะเบียน SSE2 (กว้าง 128 บิต) เพื่อจัดการกับความผันผวนที่ยาวนาน อย่างไรก็ตามการพิจารณาความผันผวนเป็นล็อคอาจนำไปสู่ข้อบกพร่องที่น่ารังเกียจในรหัสของคุณ
Michael Barker

2
ความหมายที่สำคัญที่ใช้ร่วมกันโดยล็อคตัวแปรระเหยคือพวกเขาทั้งสองจัดเตรียม Happens-Before edge (Java 1.5 และใหม่กว่า) การเข้าสู่บล็อกที่ถูกซิงโครไนซ์การนำเอาการล็อกและการอ่านจากการระเหยนั้นทั้งหมดถูกพิจารณาว่าเป็น "การได้มา" และการปลดล็อคการออกจากการบล็อกแบบซิงโครไนซ์
Michael Barker

20

synchronizedเป็นตัวดัดแปลงข้อ จำกัด การเข้าถึงระดับเมธอด / บล็อก มันจะทำให้แน่ใจว่าหนึ่งเธรดเป็นเจ้าของการล็อกสำหรับส่วนที่สำคัญ เฉพาะเธรดซึ่งเป็นเจ้าของล็อคเท่านั้นที่สามารถเข้าสู่synchronizedบล็อก หากเธรดอื่นพยายามเข้าถึงส่วนที่สำคัญนี้พวกเขาต้องรอจนกว่าเจ้าของปัจจุบันจะปลดล็อก

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

ตัวอย่างที่ดีในการใช้ตัวแปรระเหย: Dateตัวแปร

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

ป้อนคำอธิบายรูปภาพที่นี่

ดูที่บทความนี้เพื่อทำความเข้าใจvolatileแนวคิด

ลอว์เร Dol read-write-update queryเคลียร์อธิบายของคุณ

เกี่ยวกับข้อความค้นหาอื่น ๆ ของคุณ

เมื่อใดที่เหมาะสมที่จะประกาศตัวแปรที่มีความผันผวนมากกว่าเข้าถึงผ่านการซิงโครไนซ์

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

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

คำตอบจะเหมือนกับในแบบสอบถามแรก

อ้างถึงบทความนี้เพื่อความเข้าใจที่ดีขึ้น


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

11

tl; dr :

มี 3 ประเด็นหลักเกี่ยวกับมัลติเธรด:

1) เงื่อนไขการแข่งขัน

2) หน่วยความจำแคช / เก่า

3) การเพิ่มประสิทธิภาพ Complier และ CPU

volatileสามารถแก้ปัญหา 2 & 3 ได้ แต่ไม่สามารถแก้ได้ 1. synchronized/ ล็อคอย่างชัดเจนสามารถแก้ได้ 1, 2 และ 3

การทำอย่างละเอียด :

1) พิจารณาโค้ดที่ไม่ปลอดภัยของชุดข้อความนี้:

x++;

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

การใช้synchronizedโค้ดบล็อกทำให้อะตอมมีความหมายทำให้มันราวกับว่าทั้ง 3 การทำงานเกิดขึ้นพร้อมกันและไม่มีหนทางใดที่เธรดอื่นจะมาอยู่ตรงกลางและแทรกแซง ดังนั้นถ้าxเป็น 1 และ 2 เธรดพยายามให้ preform ที่x++เรารู้จักในตอนท้ายมันจะเท่ากับ 3 ดังนั้นมันจึงแก้ปัญหาสภาพการแข่งขัน

synchronized (this) {
   x++; // no problem now
}

การทำเครื่องหมายxว่าvolatileไม่สร้างx++;อะตอมดังนั้นจึงไม่สามารถแก้ปัญหานี้ได้

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

พิจารณาเรื่องนั้นในหัวข้อเดียวx = 10;. และค่อนข้างช้าในหัวข้ออื่น, x = 20;. การเปลี่ยนแปลงค่าของxอาจไม่ปรากฏในเธรดแรกเนื่องจากเธรดอื่นได้บันทึกค่าใหม่ลงในหน่วยความจำที่ใช้งานได้ แต่ไม่ได้คัดลอกไปยังหน่วยความจำหลัก หรือว่าได้คัดลอกไปยังหน่วยความจำหลัก แต่เธรดแรกยังไม่ได้อัปเดตสำเนาที่ใช้งานได้ ดังนั้นหากในขณะนี้การตรวจสอบด้ายแรกคำตอบจะif (x == 20)false

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

โปรดทราบว่าไม่เหมือนกับการจัดเก็บข้อมูลหน่วยความจำเก่าไม่ใช่เรื่องง่ายที่จะสร้างอีกครั้ง

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

พิจารณารหัสต่อไปนี้:

boolean b = false;
int x = 10;

void threadA() {
    x = 20;
    b = true;
}

void threadB() {
    if (b) {
        System.out.println(x);
    }
}

คุณจะคิดว่า threadB สามารถพิมพ์ 20 เท่านั้น (หรือไม่พิมพ์อะไรเลยถ้า threadB if-check ถูกเรียกใช้ก่อนที่จะตั้งค่าbเป็นจริง) ตามที่bกำหนดเป็น true หลังจากxตั้งค่าเป็น 20 เท่านั้น แต่คอมไพเลอร์ / CPU อาจตัดสินใจเรียงลำดับใหม่ threadA ในกรณีนั้น threadB สามารถพิมพ์ได้ 10 การทำเครื่องหมายbว่าvolatileรับรองว่าจะไม่ถูกจัดลำดับใหม่ (หรือถูกทิ้งในบางกรณี) ซึ่งหมายความว่า threadB สามารถพิมพ์ได้ 20 เท่านั้น (หรือไม่มีอะไรเลย) การทำเครื่องหมายเมธอดตามการซิงค์จะทำให้ได้ผลลัพธ์เดียวกัน นอกจากนี้การทำเครื่องหมายตัวแปรvolatileจะช่วยให้มั่นใจว่าจะไม่ได้รับการจัดเรียงใหม่ แต่ทุกอย่างก่อน / หลังสามารถเรียงลำดับใหม่ได้ดังนั้นการซิงโครไนซ์จึงเหมาะสมกว่าในบางสถานการณ์

โปรดทราบว่าก่อนหน้า Java 5 New Memory Model volatile ไม่สามารถแก้ปัญหานี้ได้


1
"แม้ว่ามันอาจดูเหมือนการทำงานเดียว แต่จริงๆแล้ว 3: การอ่านค่าปัจจุบันของ x จากหน่วยความจำเพิ่ม 1 ลงไปและบันทึกลงในหน่วยความจำ" - ถูกต้องเนื่องจากค่าจากหน่วยความจำต้องผ่านวงจร CPU เพื่อที่จะเพิ่ม / แก้ไข แม้ว่าสิ่งนี้จะกลายเป็นการINCดำเนินการของแอสเซมบลีเพียงครั้งเดียว แต่การดำเนินการของ CPU พื้นฐานยังคงเป็น 3 เท่าและจำเป็นต้องล็อคเพื่อความปลอดภัยของเธรด จุดดี. แม้ว่าINC/DECคำสั่งสามารถตั้งค่าสถานะ atomicly ในการชุมนุมและยังคงเป็น 1 การดำเนินการของอะตอม
Zombies

@ Zombies ดังนั้นเมื่อฉันสร้างบล็อกที่มีการซิงโครไนซ์สำหรับ x ++ มันจะเปลี่ยนเป็นอะตอมมิกที่ถูกตั้งค่าสถานะ INC / DEC หรือใช้ล็อคปกติหรือไม่
David Refaeli

ฉันไม่รู้! สิ่งที่ฉันรู้คือ INC / DEC ไม่ใช่อะตอมเพราะสำหรับ CPU มันต้องโหลดค่าและอ่านมันและเขียนมัน (ลงในหน่วยความจำ) เช่นเดียวกับการดำเนินการทางคณิตศาสตร์อื่น ๆ
Zombies
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.