JavaRush /จาวาบล็อก /Random-TH /การจัดการการไหล คำหลักระเหยและผลตอบแทน () วิธีการ

การจัดการการไหล คำหลักระเหยและผลตอบแทน () วิธีการ

เผยแพร่ในกลุ่ม
สวัสดี! เราศึกษามัลติเธรดต่อไป และวันนี้เราจะมาทำความรู้จักกับคีย์เวิร์ดใหม่ - วิธีการระเหยและอัตราผลตอบแทน () เรามาดูกันดีกว่าว่ามันคืออะไร :)

คำหลักมีความผันผวน

เมื่อสร้างแอปพลิเคชันแบบมัลติเธรด เราอาจประสบปัญหาร้ายแรงสองประการ ประการแรกในระหว่างการทำงานของแอปพลิเคชันแบบมัลติเธรดเธรดที่แตกต่างกันสามารถแคชค่าของตัวแปรได้ (เราจะพูดถึงเรื่องนี้เพิ่มเติมในการบรรยาย “ การใช้ระเหย” ) เป็นไปได้ว่าเธรดหนึ่งเปลี่ยนค่าของตัวแปร แต่เธรดที่สองไม่เห็นการเปลี่ยนแปลงนี้ เนื่องจากเธรดหนึ่งทำงานกับสำเนาตัวแปรที่แคชไว้ของตัวเอง โดยธรรมชาติแล้วผลที่ตามมาอาจร้ายแรงได้ ลองนึกภาพว่านี่ไม่ใช่แค่ "ตัวแปร" บางชนิด แต่เช่น ยอดคงเหลือของบัตรธนาคารของคุณ ซึ่งจู่ๆ ก็เริ่มกระโดดไปมาแบบสุ่ม :) ไม่น่าพอใจเลยใช่ไหม? ประการที่สอง ใน 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) {

   }
}
…หมายความว่า:
  1. มันจะถูกอ่านและเขียนแบบอะตอมมิกเสมอ แม้ว่าจะเป็น 64 บิตdoubleหรือlong.
  2. เครื่อง Java จะไม่แคชมัน ดังนั้นจึงไม่รวมสถานการณ์เมื่อ 10 เธรดทำงานกับสำเนาในเครื่องของตน
นี่คือวิธีแก้ไขปัญหาร้ายแรงสองข้อในหนึ่งคำ :)

อัตราผลตอบแทน () วิธีการ

เราได้ดูวิธีการต่างๆ ของชั้นเรียนแล้วThreadแต่มีสิ่งหนึ่งที่สำคัญที่จะเป็นเรื่องใหม่สำหรับคุณ นี่คือวิธีผลผลิต() แปลจากภาษาอังกฤษว่า "ยอมแพ้" และนั่นคือสิ่งที่วิธีนี้ทำ! การจัดการการไหล  คำหลักระเหยและวิธีผลผลิต () - 2เมื่อเราเรียกวิธี Yield บนเธรด มันจะพูดกับเธรดอื่นว่า “เอาล่ะ ฉันไม่ได้รีบร้อนเป็นพิเศษ ดังนั้นหากพวกคุณคนใดจำเป็นต้องได้รับเวลา CPU ก็รับไป ฉัน ไม่เร่งด่วน” นี่เป็นตัวอย่างง่ายๆ ของวิธีการทำงาน:
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-1Thread-2Thread-0Thread-1Thread-2Thread-2Thread-2Thread-1Thread-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 ไปยังคอนโซล การเขียนไปยังตัวแปรผันผวนเกิดขึ้นก่อนที่จะอ่านจากตัวแปรเหล่านั้น
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION