ตัวแปรคงที่ใช้ร่วมกันระหว่างเธรดหรือไม่


95

ครูของฉันในคลาส Java ระดับสูงเกี่ยวกับเธรดพูดบางอย่างที่ฉันไม่แน่ใจ

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

โดยพื้นฐานแล้วเขากล่าวว่าเป็นไปได้ที่readyจะมีการอัปเดตในเธรดหลัก แต่ไม่ใช่ในเธรดReaderThreadดังนั้นสิ่งนั้นReaderThreadจะวนซ้ำไปเรื่อย ๆ

นอกจากนี้เขายังอ้างว่าเป็นไปได้ที่โปรแกรมจะพิมพ์0หรือ42. ผมเข้าใจว่า42อาจจะมีการพิมพ์ 0แต่ไม่ เขากล่าวถึงกรณีนี้เมื่อnumberตัวแปรถูกตั้งค่าเป็นค่าเริ่มต้น

ฉันคิดว่าบางทีไม่สามารถรับประกันได้ว่าตัวแปรคงที่ถูกอัพเดตระหว่างเธรด แต่สิ่งนี้ทำให้ฉันแปลกมากสำหรับ Java การreadyระเหยสามารถแก้ไขปัญหานี้ได้หรือไม่?

เขาแสดงรหัสนี้:

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

การมองเห็นตัวแปรที่ไม่ใช่โลคัลไม่ได้ขึ้นอยู่กับว่าเป็นตัวแปรคงที่ฟิลด์ออบเจ็กต์หรือองค์ประกอบอาร์เรย์ทั้งหมดมีข้อควรพิจารณาเหมือนกัน (ด้วยปัญหาที่องค์ประกอบอาร์เรย์ไม่สามารถทำให้ระเหยได้)
Paŭlo Ebermann

1
ถามอาจารย์ของคุณว่าเขาคิดว่าสถาปัตยกรรมแบบไหนที่จะเห็น '0' ได้ แต่ในทางทฤษฎีเขาพูดถูก
bestsss

4
@bestsss การถามคำถามแบบนั้นจะทำให้ครูรู้ว่าเขาพลาดประเด็นทั้งหมดที่เขาพูด ประเด็นคือโปรแกรมเมอร์ที่มีความสามารถเข้าใจสิ่งที่รับประกันและสิ่งที่ไม่ใช่และอย่าพึ่งพาสิ่งที่ไม่รับประกันอย่างน้อยก็ไม่เข้าใจอย่างถ่องแท้ว่าอะไรไม่รับประกันและทำไม
David Schwartz

พวกเขาใช้ร่วมกันระหว่างทุกสิ่งที่โหลดโดยตัวโหลดคลาสเดียวกัน รวมทั้งเธรด
user207421

ครูของคุณ (และคำตอบที่ได้รับการยอมรับ) นั้นถูกต้อง 100% แต่ฉันจะบอกว่ามันไม่ค่อยเกิดขึ้น - นี่คือปัญหาที่จะซ่อนตัวมาหลายปีและจะแสดงตัวเมื่อมันจะเป็นอันตรายที่สุดเท่านั้น แม้แต่การทดสอบสั้น ๆ ที่พยายามเปิดเผยปัญหาก็มักจะทำราวกับว่าทุกอย่างเรียบร้อยดี (อาจเป็นเพราะพวกเขาไม่มีเวลาให้ JVM ทำการเพิ่มประสิทธิภาพมากนัก) ดังนั้นจึงเป็นประเด็นที่ดีที่ควรระวัง
Bill K

คำตอบ:


76

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

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

นี่คือคำพูดจากลิงค์นั้น (ให้ไว้ในความคิดเห็นโดย Jed Wesley-Smith):

บทที่ 17 ของข้อกำหนดภาษา Java กำหนดความสัมพันธ์ที่เกิดขึ้นก่อนการดำเนินการหน่วยความจำเช่นการอ่านและเขียนตัวแปรที่ใช้ร่วมกัน ผลลัพธ์ของการเขียนโดยเธรดหนึ่งจะรับประกันว่าเธรดอื่นจะมองเห็นได้เฉพาะในกรณีที่การดำเนินการเขียนเกิดขึ้นก่อนการดำเนินการอ่าน โครงสร้างที่ซิงโครไนซ์และผันผวนเช่นเดียวกับเมธอด Thread.start () และ Thread.join () สามารถสร้างความสัมพันธ์ที่เกิดขึ้นก่อน โดยเฉพาะอย่างยิ่ง:

  • แต่ละการดำเนินการในเธรดเกิดขึ้นก่อนทุกการกระทำในเธรดนั้นที่เกิดขึ้นในภายหลังตามลำดับของโปรแกรม

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

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

  • การเรียกให้เริ่มเธรดเกิดขึ้นก่อนการดำเนินการใด ๆ ในเธรดเริ่มต้น

  • การดำเนินการทั้งหมดในเธรดเกิดขึ้นก่อนที่เธรดอื่นจะส่งคืนจากการรวมบนเธรดนั้นได้สำเร็จ


3
ในทางปฏิบัติ "ในเวลาที่เหมาะสม" และ "เคย" มีความหมายเหมือนกัน เป็นไปได้มากที่โค้ดข้างต้นจะไม่มีวันยุติ
TREE

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

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

2
"เว้นแต่คุณจะใช้คำหลักที่ลบเลือนหรือซิงโครไนซ์" ควรอ่าน "เว้นแต่จะมีความสัมพันธ์ระหว่างผู้เขียนและผู้อ่านที่เกี่ยวข้องกัน" และลิงก์นี้: download.oracle.com/javase/6/docs/api/java/util/concurrent/…
Jed Wesley-Smith

2
@bestsss เห็นดี น่าเสียดายที่ ThreadGroup เสียในหลาย ๆ ด้าน
Jed Wesley-Smith

37

เขากำลังพูดถึงการมองเห็นและไม่ควรมองข้ามตัวตน

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

บทความนี้นำเสนอมุมมองที่สอดคล้องกับวิธีที่เขานำเสนอข้อมูล:

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

  • แต่ละเธรดใน Java เกิดขึ้นในพื้นที่หน่วยความจำแยกกัน (นี่เป็นเรื่องจริงอย่างชัดเจนดังนั้นโปรดอดทนกับฉันในเรื่องนี้)

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

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

...

แบบจำลองด้าย

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


12

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

ดังนั้น Java จึงกำหนดMemory Modelซึ่งระบุว่าเธรดในสถานการณ์ใดสามารถเห็นสถานะที่สอดคล้องกันของข้อมูลที่แบ่งใช้

ในกรณีเฉพาะของคุณการเพิ่มการvolatileรับประกันการมองเห็น


8

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

และตามทฤษฎีแล้วการเขียนโดยเธรดอื่นอาจอยู่ในลำดับที่แตกต่างกันเว้นแต่จะมีการประกาศตัวแปรvolatileหรือการเขียนจะซิงโครไนซ์อย่างชัดเจน


5

ภายใน classloader เดียวฟิลด์แบบคงที่จะถูกแชร์เสมอ อย่างชัดเจนข้อมูลขอบเขตกระทู้, ThreadLocalคุณต้องการที่จะใช้สิ่งอำนวยความสะดวกเช่น


2

เมื่อคุณเตรียมใช้งานตัวแปรประเภทดั้งเดิมคงที่ค่าเริ่มต้น java กำหนดค่าสำหรับตัวแปรคง

public static int i ;

เมื่อคุณกำหนดตัวแปรเช่นนี้ค่าเริ่มต้นของ i = 0; นั่นเป็นเหตุผลที่มีความเป็นไปได้ที่จะทำให้คุณได้ 0 จากนั้นเธรดหลักจะอัปเดตค่าของบูลีนพร้อมเป็นจริง เนื่องจากพร้อมเป็นตัวแปรคงเธรดหลักและการอ้างอิงเธรดอื่นไปยังที่อยู่หน่วยความจำเดียวกันดังนั้นตัวแปรที่พร้อมจึงเปลี่ยนไป ดังนั้นเธรดรองจึงหลุดออกจาก while loop และค่าการพิมพ์ เมื่อพิมพ์ค่าเริ่มต้นค่าของ number คือ 0 หากกระบวนการเธรดผ่านไปในขณะที่วนซ้ำก่อนตัวแปรหมายเลขอัพเดตเธรดหลัก จากนั้นมีความเป็นไปได้ที่จะพิมพ์ 0


-2

@dontocsata คุณสามารถกลับไปหาครูและโรงเรียนเขาได้เล็กน้อย :)

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

ตัวแปร 2 ตัวต่อไปนี้จะอยู่ในบรรทัดแคชเดียวกันภายใต้สถาปัตยกรรมรู้แทบทุกชนิด

private static boolean ready;  
private static int number;  

Thread.exit(เธรดหลัก) ได้รับการประกันว่าจะออกและexitรับประกันว่าจะทำให้เกิดรั้วหน่วยความจำเนื่องจากการลบเธรดกลุ่มเธรด (และปัญหาอื่น ๆ อีกมากมาย) (เป็นการโทรที่ซิงโครไนซ์และฉันไม่เห็นวิธีเดียวที่จะนำไปใช้โดยไม่มีส่วนการซิงค์เนื่องจาก ThreadGroup ต้องยุติเช่นกันหากไม่มีเธรด daemon เหลืออยู่ ฯลฯ )

เธรดเริ่มต้นReaderThreadจะทำให้กระบวนการมีชีวิตอยู่เนื่องจากไม่ใช่ daemon! ดังนั้นreadyและnumberจะถูกลบเข้าด้วยกัน (หรือตัวเลขก่อนหน้าหากเกิดการสลับบริบท) และไม่มีเหตุผลที่แท้จริงในการจัดลำดับใหม่ในกรณีนี้อย่างน้อยฉันก็คิดไม่ออก คุณจะต้องมีอะไรบางอย่างที่แปลกอย่างแท้จริงที่จะเห็นอะไร 42แต่ อีกครั้งฉันคิดว่าตัวแปรคงที่ทั้งสองจะอยู่ในบรรทัดแคชเดียวกัน ฉันนึกไม่ออกว่าแคชบรรทัดยาว 4 ไบต์หรือ JVM ที่จะไม่กำหนดให้เป็นพื้นที่ต่อเนื่อง (บรรทัดแคช)


3
@bestsss ในขณะที่เป็นจริงทั้งหมดในปัจจุบัน แต่ต้องอาศัยการใช้งาน JVM และสถาปัตยกรรมฮาร์ดแวร์ในปัจจุบันสำหรับความจริงไม่ใช่ความหมายของโปรแกรม ซึ่งหมายความว่าโปรแกรมยังคงใช้งานไม่ได้แม้ว่าอาจจะทำงานได้ เป็นเรื่องง่ายที่จะค้นหาตัวแปรที่ไม่สำคัญของตัวอย่างนี้ซึ่งล้มเหลวจริงตามวิธีที่ระบุไว้
Jed Wesley-Smith

1
ฉันบอกว่ามันไม่เป็นไปตามข้อกำหนดอย่างไรก็ตามในฐานะครูอย่างน้อยก็หาตัวอย่างที่เหมาะสมที่อาจล้มเหลวในสถาปัตยกรรมสินค้าโภคภัณฑ์ดังนั้นตัวอย่างจึงเป็นของจริง
bestsss

6
อาจเป็นคำแนะนำที่แย่ที่สุดในการเขียนรหัสเธรดปลอดภัยที่ฉันเคยเห็น
Lawrence Dol

4
@Bestsss: คำตอบง่ายๆสำหรับคำถามของคุณมีเพียงแค่นี้: "รหัสตามข้อกำหนดและเอกสารไม่ใช่ผลข้างเคียงของระบบหรือการนำไปใช้งานเฉพาะของคุณ" สิ่งนี้มีความสำคัญอย่างยิ่งในแพลตฟอร์มเครื่องเสมือนที่ออกแบบมาให้ไม่เชื่อเรื่องพระเจ้าของฮาร์ดแวร์
Lawrence Dol

1
@Bestsss: ครูชี้ให้เห็นว่า (a) รหัสอาจใช้งานได้ดีเมื่อคุณทดสอบและ (b) รหัสเสียเพราะขึ้นอยู่กับการทำงานของฮาร์ดแวร์ไม่ใช่การรับประกันข้อมูลจำเพาะ ประเด็นคือดูเหมือนว่าจะโอเค แต่ก็ไม่ได้ตกลง
Lawrence Dol
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.