สวัสดี! เราศึกษามัลติเธรดต่อไป วันนี้เรามาทำความรู้จักกับ
เมื่อเราเรียกใช้
volatile
คีย์เวิร์ดและyield()
วิธีการกัน มาดำน้ำกันเถอะ :)
คำหลักที่ผันผวน
เมื่อสร้างแอปพลิเคชันแบบมัลติเธรด เราอาจพบปัญหาร้ายแรงสองประการ อันดับแรก เมื่อแอปพลิเคชันแบบมัลติเธรดทำงาน เธรดต่างๆ สามารถแคชค่าของตัวแปรได้ (เราได้พูดถึงเรื่องนี้ไปแล้วในบทเรียนเรื่อง 'การใช้สารระเหย' ) คุณสามารถมีสถานการณ์ที่เธรดหนึ่งเปลี่ยนค่าของตัวแปร แต่เธรดที่สองไม่เห็นการเปลี่ยนแปลง เนื่องจากเธรดนั้นทำงานกับสำเนาของตัวแปรที่แคชไว้ โดยธรรมชาติแล้วผลที่ตามมาอาจร้ายแรง สมมติว่าไม่ใช่แค่ตัวแปรเก่า ๆ แต่เป็นยอดเงินในบัญชีธนาคารของคุณ ซึ่งจู่ ๆ ก็กระโดดขึ้น ๆ ลง ๆ ไปเรื่อย ๆ :) นั่นฟังดูไม่สนุกใช่ไหม ประการที่สอง ใน Java การดำเนินการเพื่ออ่านและเขียนประเภทดั้งเดิมทั้งหมดlong
double
, เป็นปรมาณู ตัวอย่างเช่น ถ้าคุณเปลี่ยนค่าของint
ตัวแปรในเธรดหนึ่ง และในอีกเธรดหนึ่ง คุณอ่านค่าของตัวแปร คุณจะได้ค่าเก่าหรือค่าใหม่ นั่นคือค่าที่เกิดจากการเปลี่ยนแปลง ในเธรด 1 ไม่มี 'ค่ากลาง' อย่างไรก็ตาม สิ่งนี้ใช้ไม่ได้กับlong
s และdouble
s ทำไม เนื่องจากการสนับสนุนข้ามแพลตฟอร์ม จำได้ไหมว่าในระดับเริ่มต้นที่เรากล่าวว่าหลักการชี้นำของ Java คือ 'เขียนครั้งเดียว รันได้ทุกที่'? นั่นหมายถึงการสนับสนุนข้ามแพลตฟอร์ม กล่าวอีกนัยหนึ่ง แอปพลิเคชัน Java ทำงานบนแพลตฟอร์มต่างๆ ทุกประเภท ตัวอย่างเช่น บนระบบปฏิบัติการ Windows, Linux หรือ MacOS เวอร์ชันต่างๆ มันจะทำงานโดยไม่มีข้อผูกมัดกับพวกเขาทั้งหมด การชั่งน้ำหนัก 64 บิตlong
double
เป็นพื้นฐานที่ 'หนักที่สุด' ใน Java และบางแพลตฟอร์มแบบ 32 บิตก็ไม่ได้ใช้การอ่านและเขียนตัวแปรแบบ 64 บิตในระดับอะตอม ตัวแปรดังกล่าวถูกอ่านและเขียนในสองการดำเนินการ ขั้นแรก 32 บิตแรกจะถูกเขียนลงในตัวแปร และจากนั้นอีก 32 บิตจะถูกเขียน เป็นผลให้เกิดปัญหาขึ้นได้ หนึ่งเธรดเขียนค่า 64 บิตให้กับX
ตัวแปร และเขียนในสองการดำเนินการ ในขณะเดียวกัน เธรดที่สองจะพยายามอ่านค่าของตัวแปรและดำเนินการระหว่างการดำเนินการทั้งสองนั้น เมื่อ 32 บิตแรกถูกเขียน แต่ 32 บิตที่สองไม่ได้เขียน เป็นผลให้อ่านค่ากลางที่ไม่ถูกต้อง และเรามีจุดบกพร่อง ตัวอย่างเช่น หากบนแพลตฟอร์มดังกล่าว เราพยายามเขียนหมายเลขเป็น 9223372036854775809 สำหรับตัวแปร มันจะครอบครอง 64 บิต ในรูปแบบไบนารีจะมีลักษณะดังนี้: 1000000000000000000000000000000000000000000000000000000000000001 เธรดแรกเริ่มเขียนตัวเลขไปยังตัวแปร ในตอนแรกจะเขียน 32 บิตแรก (1000000000000000000000000000000) และ 32 บิตที่สอง (0000000000000000000000000000001) และเธรดที่สองสามารถถูกตรึงระหว่างการดำเนินการเหล่านี้ โดยอ่านค่ากลางของตัวแปร (10000000000000000000000000000000) ซึ่งเป็น 32 บิตแรกที่เขียนไปแล้ว ในระบบเลขฐานสิบ ตัวเลขนี้คือ 2,147,483,648 กล่าวอีกนัยหนึ่ง เราแค่ต้องการเขียนหมายเลข 9223372036854775809 ลงในตัวแปร แต่เนื่องจากการดำเนินการนี้ไม่ใช่ปรมาณูในบางแพลตฟอร์ม เราจึงมีหมายเลขชั่วร้าย 2,147,483,648 ซึ่งมาจากไหนก็ไม่รู้และจะมีผลกระทบที่ไม่รู้จัก โปรแกรม. เธรดที่สองอ่านค่าของตัวแปรก่อนที่จะเขียนเสร็จ กล่าวคือ เธรดเห็น 32 บิตแรก แต่ไม่เห็น 32 บิตที่สอง แน่นอนว่าปัญหาเหล่านี้ไม่ได้เกิดขึ้นเมื่อวานนี้ Java แก้ปัญหาด้วยคำหลักเดียว: volatile
. ถ้าเราใช้volatile
คำหลักเวลาประกาศตัวแปรบางตัวในโปรแกรมของเรา...
public class Main {
public volatile long x = 2222222222222222222L;
public static void main(String[] args) {
}
}
…หมายความว่า:
- มันจะถูกอ่านและเขียนแบบปรมาณูเสมอ แม้ว่าจะเป็น 64 บิต
double
หรือlong
. - เครื่อง Java จะไม่แคช ดังนั้นคุณจะไม่มีสถานการณ์ที่ 10 เธรดทำงานกับสำเนาในเครื่องของตนเอง
วิธีผลตอบแทน ()
เราได้ตรวจสอบวิธีการของคลาสต่างๆ แล้วThread
แต่มีวิธีการที่สำคัญที่จะใหม่สำหรับคุณ มันเป็นวิธีyield()
การ และมันทำตามชื่อของมันทุกประการ! 
yield
เมธอดในเธรด อันที่จริงแล้วมันจะพูดคุยกับเธรดอื่นๆ: 'เฮ้ พวก ฉันไม่ได้รีบไปไหนเป็นพิเศษ ดังนั้นหากคุณคนใดต้องการเวลาในการประมวลผลก็รับไป ฉันรอได้' นี่คือตัวอย่างง่ายๆ ของวิธีการทำงาน:
public class ThreadExample extends Thread {
public ThreadExample() {
this.start();
}
public void run() {
System.out.println(Thread.currentThread().getName() + " yields its place to others");
Thread.yield();
System.out.println(Thread.currentThread().getName() + " has finished executing.");
}
public static void main(String[] args) {
new ThreadExample();
new ThreadExample();
new ThreadExample();
}
}
เราสร้างและเริ่มสามเธรดตามลำดับ: , Thread-0
, Thread-1
และ เริ่มก่อนและยอมจำนนต่อผู้อื่นทันที จากนั้นจะเริ่มต้นและให้ผลตอบแทนด้วย จากนั้นจะเริ่มต้นซึ่งยังให้ผล เราไม่มีเธรดอีกต่อไปแล้ว และหลังจากที่ได้ตำแหน่งสุดท้ายแล้ว ตัวจัดกำหนดการเธรดก็พูดว่า 'อืม ไม่มีเธรดใหม่อีกแล้ว คิวเรามีใครบ้าง ใครยอมใครก่อน? ดูเหมือนว่าจะเป็น ตกลงนั่นหมายความว่าเราจะปล่อยให้มันทำงาน ' เสร็จสิ้นการทำงาน จากนั้นตัวกำหนดตารางเวลาเธรดจะดำเนินการประสานงานต่อไป: 'เอาล่ะเสร็จแล้ว เรามีคนอื่นอยู่ในคิวหรือไม่'. Thread-0 อยู่ในคิว: มันให้ตำแหน่งมาก่อนThread-2
Thread-0
Thread-1
Thread-2
Thread-2
Thread-2
Thread-1
Thread-1
Thread-1
Thread-1
. ตอนนี้ถึงเวลาแล้วและดำเนินการให้เสร็จสิ้น จากนั้นตัวกำหนดตารางเวลาก็เสร็จสิ้นการประสานเธรด: 'โอเคThread-2
คุณยอมจำนนต่อเธรดอื่น และตอนนี้พวกมันก็เสร็จเรียบร้อยแล้ว คุณเป็นคนสุดท้ายที่ยอมจำนน ตอนนี้ถึงตาคุณแล้ว' แล้วThread-2
ดำเนินการให้เสร็จสิ้น เอาต์พุตของคอนโซลจะมีลักษณะดังนี้: Thread-0 ให้ตำแหน่งแก่ผู้อื่น Thread-1 ให้ตำแหน่งแก่ผู้อื่น Thread-2 ให้ตำแหน่งแก่ผู้อื่น Thread-1 ดำเนินการเสร็จสิ้นแล้ว Thread-0 ดำเนินการเสร็จสิ้นแล้ว Thread-2 ดำเนินการเสร็จสิ้นแล้ว แน่นอน ตัวกำหนดตารางเวลาของเธรดอาจเริ่มเธรดในลำดับที่แตกต่างกัน (เช่น 2-1-0 แทนที่จะเป็น 0-1-2) แต่หลักการยังคงเหมือนเดิม
เกิดขึ้นก่อนกฎ
สิ่งสุดท้ายที่เราจะพูดถึงในวันนี้คือแนวคิดของ ' เกิดขึ้นก่อน ' ดังที่คุณทราบแล้วใน Java ตัวกำหนดตารางเวลาของเธรดจะทำงานจำนวนมากที่เกี่ยวข้องกับการจัดสรรเวลาและทรัพยากรให้กับเธรดเพื่อดำเนินการงานของพวกเขา คุณยังได้เห็นซ้ำๆ ว่าเธรดถูกดำเนินการตามลำดับแบบสุ่มซึ่งโดยปกติแล้วจะไม่สามารถคาดเดาได้อย่างไร และโดยทั่วไปแล้ว หลังจากการเขียนโปรแกรม 'แบบต่อเนื่อง' ที่เราทำก่อนหน้านี้ การเขียนโปรแกรมแบบมัลติเธรดจะดูเหมือนเป็นการสุ่ม คุณเชื่อแล้วว่าคุณสามารถใช้วิธีต่างๆ เพื่อควบคุมโฟลว์ของโปรแกรมแบบมัลติเธรดได้ แต่การทำงานแบบมัลติเธรดใน Java มีเสาหลักอีก 1 เสา นั่นคือ กฎ 4 ข้อ ' happen-before ' การทำความเข้าใจกฎเหล่านี้ค่อนข้างง่าย ลองนึกภาพว่าเรามีสองเธรด —A
และB
. แต่ละเธรดเหล่านี้สามารถดำเนินการ1
และ 2
ในแต่ละกฎ เมื่อเราพูดว่า ' A เกิดขึ้นก่อน B ' เราหมายความว่าการเปลี่ยนแปลงทั้งหมดที่ทำโดยเธรดA
ก่อนการดำเนินการ1
และการเปลี่ยนแปลงที่เป็นผลมาจากการดำเนินการนี้จะมองเห็นได้ในเธรดB
เมื่อการดำเนินการ2
ถูกดำเนินการและหลังจากนั้น กฎแต่ละข้อรับประกันว่าเมื่อคุณเขียนโปรแกรมแบบมัลติเธรด เหตุการณ์บางอย่างจะเกิดขึ้นก่อนเหตุการณ์อื่น 100% และในขณะดำเนินการ2
เธรดB
จะรับรู้ถึงการเปลี่ยนแปลงที่A
เกิดขึ้นระหว่าง เธรด 1
เสมอ ลองทบทวนพวกเขา
กฎข้อที่ 1
การปล่อย mutex เกิดขึ้นก่อนที่เธรดอื่นจะได้รับจอภาพเดียวกัน ฉันคิดว่าคุณเข้าใจทุกอย่างที่นี่ หาก mutex ของอ็อบเจ็กต์หรือคลาสได้รับโดยเธรดหนึ่ง ตัวอย่างเช่น โดยเธรดA
เธรดอื่น (เธรดB
) จะไม่สามารถรับได้ในเวลาเดียวกัน ต้องรอจนกว่า mutex จะเปิดตัว
กฎข้อที่ 2
วิธีThread.start()
การเกิดขึ้นก่อน Thread.run()
. อีกครั้งไม่มีอะไรยากที่นี่ คุณรู้อยู่แล้วว่าในการเริ่มรันโค้ดภายในrun()
เมธอด คุณต้องเรียกstart()
เมธอดในเธรด โดยเฉพาะวิธีการเริ่มต้น ไม่ใช่run()
วิธีการเอง! กฎนี้ทำให้แน่ใจว่าค่าของตัวแปรทั้งหมดที่ตั้งไว้ก่อนจะThread.start()
ถูกเรียกใช้จะมองเห็นได้ภายในrun()
เมธอดเมื่อเริ่มต้น
กฎข้อที่ 3
การสิ้นสุดของrun()
เมธอดเกิดขึ้นก่อนที่จะกลับมาจากjoin()
เมธอด กลับไปที่สองหัวข้อของเรา: A
และB
. เราเรียกjoin()
เมธอดเพื่อให้เธรดB
ได้รับการรับประกันว่าจะรอให้เธรดเสร็จสิ้นA
ก่อนที่จะทำงาน ซึ่งหมายความว่าเมธอดของวัตถุ A run()
จะทำงานจนจบ และการเปลี่ยนแปลงข้อมูลทั้งหมดที่เกิดขึ้นในrun()
เมธอดของเธรดA
นั้นรับประกันหนึ่งร้อยเปอร์เซ็นต์ว่าจะมองเห็นได้ในเธรดB
เมื่อเสร็จสิ้นการรอให้เธรดA
ทำงานเสร็จจึงจะสามารถเริ่มงานของตัวเองได้
กฎข้อที่ 4
การเขียนไปยังvolatile
ตัวแปรจะเกิดขึ้นก่อนที่จะอ่านจากตัวแปรเดียวกันนั้น เมื่อเราใช้volatile
คำหลัก เราจะได้ค่าปัจจุบันเสมอ แม้จะมี a long
หรือdouble
(เราได้พูดถึงปัญหาที่อาจเกิดขึ้นก่อนหน้านี้แล้ว) ตามที่คุณเข้าใจแล้ว การเปลี่ยนแปลงที่เกิดขึ้นในบางเธรดจะไม่ปรากฏให้เห็นในเธรดอื่นเสมอไป แต่แน่นอนว่ามีสถานการณ์บ่อยครั้งมากที่พฤติกรรมดังกล่าวไม่เหมาะกับเรา สมมติว่าเรากำหนดค่าให้กับตัวแปรบนเธรดA
:
int z;
….
z = 555;
หากB
เธรดของเราควรแสดงค่าของz
ตัวแปรบนคอนโซล เธรดสามารถแสดงเป็น 0 ได้ง่าย เนื่องจากไม่ทราบเกี่ยวกับค่าที่กำหนด แต่กฎข้อที่ 4 รับประกันว่าหากเราประกาศz
ตัวแปรเป็น การvolatile
เปลี่ยนแปลงค่าในเธรดหนึ่งจะมองเห็นได้ในอีกเธรดหนึ่งเสมอ หากเราเพิ่มคำvolatile
ในโค้ดก่อนหน้า...
volatile int z;
….
z = 555;
...จากนั้นเราจะป้องกันสถานการณ์ที่เธรดB
อาจแสดงเป็น 0 การเขียนไปยังvolatile
ตัวแปรจะเกิดขึ้นก่อนที่จะอ่านจากตัวแปรเหล่านั้น
GO TO FULL VERSION