สวัสดี! เราศึกษามัลติเธรดต่อไป และวันนี้เราจะมาทำความรู้จักกับคีย์เวิร์ดใหม่ - วิธีการระเหยและอัตราผลตอบแทน () เรามาดูกันดีกว่าว่ามันคืออะไร :)
เมื่อเราเรียกวิธี Yield บนเธรด มันจะพูดกับเธรดอื่นว่า “เอาล่ะ ฉันไม่ได้รีบร้อนเป็นพิเศษ ดังนั้นหากพวกคุณคนใดจำเป็นต้องได้รับเวลา CPU ก็รับไป ฉัน ไม่เร่งด่วน” นี่เป็นตัวอย่างง่ายๆ ของวิธีการทำงาน:
คำหลักมีความผันผวน
เมื่อสร้างแอปพลิเคชันแบบมัลติเธรด เราอาจประสบปัญหาร้ายแรงสองประการ ประการแรกในระหว่างการทำงานของแอปพลิเคชันแบบมัลติเธรดเธรดที่แตกต่างกันสามารถแคชค่าของตัวแปรได้ (เราจะพูดถึงเรื่องนี้เพิ่มเติมในการบรรยาย “ การใช้ระเหย” ) เป็นไปได้ว่าเธรดหนึ่งเปลี่ยนค่าของตัวแปร แต่เธรดที่สองไม่เห็นการเปลี่ยนแปลงนี้ เนื่องจากเธรดหนึ่งทำงานกับสำเนาตัวแปรที่แคชไว้ของตัวเอง โดยธรรมชาติแล้วผลที่ตามมาอาจร้ายแรงได้ ลองนึกภาพว่านี่ไม่ใช่แค่ "ตัวแปร" บางชนิด แต่เช่น ยอดคงเหลือของบัตรธนาคารของคุณ ซึ่งจู่ๆ ก็เริ่มกระโดดไปมาแบบสุ่ม :) ไม่น่าพอใจเลยใช่ไหม? ประการที่สอง ใน Java การดำเนินการอ่านและเขียนบนฟิลด์ทุกประเภท ยกเว้นlong
และdouble
เป็นอะตอมมิก อะตอมมิกคืออะไร? ตัวอย่างเช่น หากคุณเปลี่ยนค่าของตัวแปรในเธรดหนึ่งint
และในอีกเธรดหนึ่งที่คุณอ่านค่าของตัวแปรนี้ คุณจะได้รับค่าเก่าหรือค่าใหม่ - ค่าที่ปรากฏหลังจากการเปลี่ยนแปลงใน เธรด 1. ไม่มี “ตัวเลือกระดับกลาง” ปรากฏขึ้นที่นั่นอาจจะ. อย่างไรก็ตามสิ่งนี้ใช้ไม่ได้ กับ long
และ double
ทำไม เพราะมันข้ามแพลตฟอร์ม คุณจำที่เราพูดในระดับแรกๆ ได้ไหมว่าหลักการของ Java นั้น “เขียนครั้งเดียว ใช้งานได้ทุกที่”? นี่คือข้ามแพลตฟอร์ม นั่นคือแอปพลิเคชัน Java ทำงานบนแพลตฟอร์มที่แตกต่างกันโดยสิ้นเชิง ตัวอย่างเช่น บนระบบปฏิบัติการ Windows, Linux หรือ MacOS เวอร์ชันต่างๆ และทุกที่ที่แอปพลิเคชันนี้จะทำงานได้เสถียร long
และdouble
- พื้นฐานที่ "หนัก" ที่สุดใน Java: มีน้ำหนัก 64 บิต และแพลตฟอร์ม 32 บิตบางแพลตฟอร์มไม่ได้ใช้อะตอมมิกซิตีของการอ่านและการเขียนตัวแปร 64 บิต ตัวแปรดังกล่าวสามารถอ่านและเขียนได้ในการดำเนินการสองครั้ง ขั้นแรก 32 บิตแรกจะถูกเขียนไปยังตัวแปร จากนั้นอีก 32 บิต ดังนั้น ในกรณีเหล่านี้อาจเกิดปัญหาขึ้น หนึ่งเธรดเขียนค่า 64 บิตให้กับตัวแปรХ
และเขาทำมัน “ในสองขั้นตอน” ในเวลาเดียวกัน เธรดที่สองพยายามอ่านค่าของตัวแปรนี้ และทำตรงกลางเมื่อมีการเขียน 32 บิตแรกแล้ว แต่บิตที่สองยังไม่ได้เขียน เป็นผลให้อ่านค่ากลางที่ไม่ถูกต้องและเกิดข้อผิดพลาด ตัวอย่างเช่นหากบนแพลตฟอร์มดังกล่าวเราพยายามเขียนตัวเลขลงในตัวแปร - 9223372036854775809 - มันจะครอบครอง 64 บิต ในรูปแบบไบนารี่จะมีลักษณะดังนี้: 1000000000000000000000000000000000000000000000000000000000000001 เธรดแรกจะเริ่มเขียนตัวเลขนี้ไปยังตัวแปร และจะเขียน 32 บิตแรก: 10000000000000000000 0000000 00000 และ 32 ที่สอง: 00000000000000000000000000000001 และเธรดที่สองสามารถแทรกเข้าไปในช่องว่างนี้และ อ่านค่ากลางของตัวแปร - 100000000000000000000000000000000 ซึ่งเป็น 32 บิตแรกที่เขียนไปแล้ว ในระบบทศนิยมตัวเลขนี้เท่ากับ 2147483648 นั่นคือเราแค่อยากเขียนหมายเลข 9223372036854775809 ลงในตัวแปร แต่เนื่องจากการดำเนินการนี้บนบางแพลตฟอร์มไม่ใช่อะตอมมิก เราจึงได้หมายเลข "ซ้าย" 2147483648 ที่เราไม่ต้องการ มาจากไหนก็ไม่รู้ และจะส่งผลต่อการทำงานของโปรแกรมอย่างไร เธรดที่สองเพียงอ่านค่าของตัวแปรก่อนที่จะถูกเขียนในที่สุด นั่นคือ เห็น 32 บิตแรก แต่ไม่ใช่ 32 บิตที่สอง แน่นอนว่าปัญหาเหล่านี้ไม่ได้เกิดขึ้นเมื่อวานนี้และใน Java พวกเขาได้รับการแก้ไขโดยใช้คำหลักเพียงคำเดียว - ระเหยได้ หากเราประกาศตัวแปรบางตัวในโปรแกรมด้วยคำว่า volatile...
public class Main {
public volatile long x = 2222222222222222222L;
public static void main(String[] args) {
}
}
…หมายความว่า:
- มันจะถูกอ่านและเขียนแบบอะตอมมิกเสมอ แม้ว่าจะเป็น 64 บิต
double
หรือlong
. - เครื่อง Java จะไม่แคชมัน ดังนั้นจึงไม่รวมสถานการณ์เมื่อ 10 เธรดทำงานกับสำเนาในเครื่องของตน
อัตราผลตอบแทน () วิธีการ
เราได้ดูวิธีการต่างๆ ของชั้นเรียนแล้วThread
แต่มีสิ่งหนึ่งที่สำคัญที่จะเป็นเรื่องใหม่สำหรับคุณ นี่คือวิธีผลผลิต() แปลจากภาษาอังกฤษว่า "ยอมแพ้" และนั่นคือสิ่งที่วิธีนี้ทำ! 
public class ThreadExample extends Thread {
public ThreadExample() {
this.start();
}
public void run() {
System.out.println(Thread.currentThread().getName() + "give way 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-1 บัดนี้เรื่องมาถึงเขาแล้ว และเขากำลังดำเนินการจนถึงที่สุด หลังจากนั้นผู้จัดตารางเวลาจะประสานงานเธรดให้เสร็จ: “เอาล่ะ เธรด-2 คุณเปิดทางให้กับเธรดอื่นแล้ว เธรดเหล่านั้นทำงานได้หมดแล้ว คุณเป็นคนสุดท้ายที่ยอมแพ้ ดังนั้นตอนนี้ถึงตาคุณแล้ว” หลังจากนี้ Thread-2 จะทำงานจนเสร็จสิ้น เอาต์พุตคอนโซลจะมีลักษณะดังนี้: Thread-0 ให้ทางแก่ผู้อื่น Thread-1 ให้ทางแก่ผู้อื่น Thread-2 ให้ทางแก่ผู้อื่น Thread-1 ได้ดำเนินการเสร็จแล้ว Thread-0 ดำเนินการเสร็จแล้ว Thread-2 ดำเนินการเสร็จสิ้นแล้ว แน่นอนว่าตัวกำหนดเวลาเธรดสามารถรันเธรดในลำดับอื่นได้ (เช่น 2-1-0 แทนที่จะเป็น 0-1-2) แต่หลักการก็เหมือนกัน Thread-1
Thread-2
Thread-0
Thread-1
Thread-2
Thread-2
Thread-2
Thread-1
Thread-1
เกิดขึ้นก่อนกฎเกณฑ์
สิ่งสุดท้ายที่เราจะพูดถึงในวันนี้คือหลักการ " เกิดขึ้นก่อน " ดังที่คุณทราบแล้วว่าใน Java งานส่วนใหญ่ในการจัดสรรเวลาและทรัพยากรให้กับเธรดเพื่อทำงานให้สำเร็จนั้นทำโดยตัวกำหนดเวลาเธรด นอกจากนี้ คุณได้เห็นมากกว่าหนึ่งครั้งว่าเธรดถูกดำเนินการอย่างไรตามลำดับที่กำหนดเอง และส่วนใหญ่มักจะเป็นไปไม่ได้ที่จะคาดเดาได้ และโดยทั่วไป หลังจากการเขียนโปรแกรมแบบ "ต่อเนื่อง" ที่เราทำก่อนหน้านี้ การทำงานแบบมัลติเธรดดูเหมือนเป็นเรื่องสุ่ม ดังที่คุณเห็นแล้ว ความคืบหน้าของโปรแกรมแบบมัลติเธรดสามารถควบคุมได้โดยใช้วิธีการทั้งชุด แต่นอกเหนือจากนี้ใน Java multithreading ยังมี "เกาะแห่งเสถียรภาพ" อีกอันหนึ่ง - กฎ 4 ข้อที่เรียกว่า " เกิดขึ้นก่อน " ในภาษาอังกฤษแปลว่า "เกิดขึ้นก่อน" หรือ "เกิดขึ้นก่อน" ความหมายของกฎเหล่านี้ค่อนข้างเข้าใจง่าย ลองนึกภาพว่าเรามีสองเธรด -A
และB
. แต่ละเธรดเหล่านี้สามารถดำเนินการ1
และ2
. และเมื่อในแต่ละกฎเราพูดว่า " A เกิดขึ้นก่อน B " ซึ่งหมายความว่าการเปลี่ยนแปลงทั้งหมดที่ทำโดยเธรดA
ก่อนการดำเนินการ1
และการเปลี่ยนแปลงที่การดำเนินการนี้นำมาซึ่งจะปรากฏให้เห็นในเธรดB
ณ เวลาที่ดำเนินการ2
และ หลังจากดำเนินการแล้ว กฎแต่ละข้อเหล่านี้ช่วยให้แน่ใจว่าเมื่อเขียนโปรแกรมแบบมัลติเธรด เหตุการณ์บางอย่างจะเกิดขึ้นก่อนเหตุการณ์อื่น 100% และเธรดB
ณ เวลาดำเนินการ2
จะรับรู้ถึงการเปลี่ยนแปลงที่เธรดА
ทำระหว่างการดำเนินการ เสมอ 1
. มาดูพวกเขากันดีกว่า
กฎข้อที่ 1
การปล่อย mutex เกิดขึ้นก่อนจะเกิดขึ้นก่อนที่เธรดอื่นจะได้รับมอนิเตอร์เดียวกัน ทุกอย่างดูชัดเจนที่นี่ หากได้รับ mutex ของอ็อบเจ็กต์หรือคลาสโดยเธรดหนึ่ง ตัวอย่างเช่น เธรด เธรดА
อื่น (thread B
) จะไม่สามารถรับเธรดนั้นในเวลาเดียวกันได้ คุณต้องรอจนกว่า mutex จะออก
กฎข้อที่ 2
Thread.start()
เกิดขึ้นก่อน วิธีThread.run()
การ ไม่มีอะไรซับซ้อนเช่นกัน คุณรู้อยู่แล้วว่า: เพื่อให้โค้ดภายในเมธอดเริ่มดำเนินการได้run()
คุณต้องเรียกเมธอดบนstart()
เธรด มันเป็นของเขา ไม่ใช่วิธีการของมันเองrun()
! กฎนี้ช่วยให้แน่ใจว่าThread.start()
ค่าของตัวแปรทั้งหมดที่ตั้งค่าก่อนการดำเนินการจะมองเห็นได้ภายในวิธีการที่เริ่มดำเนินrun()
การ
กฎข้อที่ 3
วิธีการเสร็จสิ้นrun()
เกิดขึ้นก่อนที่จะออกจากวิธีjoin()
การ กลับไปที่ลำธารทั้งสองของเรา - А
และB
. เราเรียกเมธอดjoin()
ในลักษณะที่เธรดB
ต้องรอจนกว่าจะเสร็จสิ้นA
ก่อนจึงจะทำงานได้ ซึ่งหมายความว่าวิธีการrun()
ของวัตถุ A จะทำงานจนถึงจุดสิ้นสุดอย่างแน่นอน และการเปลี่ยนแปลงทั้งหมดในข้อมูลที่เกิดขึ้นใน วิธี run()
เธรดA
จะมองเห็นได้อย่างสมบูรณ์ในเธรดB
เมื่อรอให้เสร็จสิ้นA
และเริ่มทำงานเอง
กฎข้อที่ 4
การเขียนไปยังตัวแปรผันผวนจะเกิดขึ้นก่อนที่จะอ่านจากตัวแปรเดียวกัน เมื่อใช้คีย์เวิร์ด volatile เราจะได้ค่าปัจจุบันเสมอ แม้ในกรณีของlong
และdouble
ปัญหาที่มีการพูดคุยกันก่อนหน้านี้ ตามที่คุณเข้าใจแล้ว การเปลี่ยนแปลงที่เกิดขึ้นในบางเธรดจะไม่ปรากฏแก่เธรดอื่นเสมอไป แต่แน่นอนว่า บ่อยครั้งมีสถานการณ์ที่พฤติกรรมของโปรแกรมดังกล่าวไม่เหมาะกับเรา สมมติว่าเรากำหนดค่าให้กับตัวแปรในเธรดA
:
int z;
….
z= 555;
หากเธรดของเราB
ต้องพิมพ์ค่าของตัวแปรz
ไปยังคอนโซล ก็สามารถพิมพ์ 0 ได้อย่างง่ายดายเนื่องจากไม่ทราบเกี่ยวกับค่าที่กำหนดให้กับตัวแปรนั้น ดังนั้นกฎข้อที่ 4 รับประกันเราว่า: หากคุณประกาศz
ตัวแปรว่ามีความผันผวน การเปลี่ยนแปลงค่าในเธรดหนึ่งจะมองเห็นได้ในเธรดอื่นเสมอ ถ้าเราเพิ่มคำว่า volatile เข้าไปในโค้ดก่อนหน้า...
volatile int z;
….
z= 555;
...ไม่รวมสถานการณ์ที่สตรีมB
จะส่งออก 0 ไปยังคอนโซล การเขียนไปยังตัวแปรผันผวนเกิดขึ้นก่อนที่จะอ่านจากตัวแปรเหล่านั้น
GO TO FULL VERSION