CodeGym /จาวาบล็อก /สุ่ม /การจัดการเธรด คำหลักที่ผันผวนและวิธีผลตอบแทน ()
John Squirrels
ระดับ
San Francisco

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

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

คำหลักที่ผันผวน

เมื่อสร้างแอปพลิเคชันแบบมัลติเธรด เราอาจพบปัญหาร้ายแรงสองประการ อันดับแรก เมื่อแอปพลิเคชันแบบมัลติเธรดทำงาน เธรดต่างๆ สามารถแคชค่าของตัวแปรได้ (เราได้พูดถึงเรื่องนี้ไปแล้วในบทเรียนเรื่อง 'การใช้สารระเหย' ) คุณสามารถมีสถานการณ์ที่เธรดหนึ่งเปลี่ยนค่าของตัวแปร แต่เธรดที่สองไม่เห็นการเปลี่ยนแปลง เนื่องจากเธรดนั้นทำงานกับสำเนาของตัวแปรที่แคชไว้ โดยธรรมชาติแล้วผลที่ตามมาอาจร้ายแรง สมมติว่าไม่ใช่แค่ตัวแปรเก่า ๆ แต่เป็นยอดเงินในบัญชีธนาคารของคุณ ซึ่งจู่ ๆ ก็กระโดดขึ้น ๆ ลง ๆ ไปเรื่อย ๆ :) นั่นฟังดูไม่สนุกใช่ไหม ประการที่สอง ใน Java การดำเนินการเพื่ออ่านและเขียนประเภทดั้งเดิมทั้งหมดlongdouble, เป็นปรมาณู ตัวอย่างเช่น ถ้าคุณเปลี่ยนค่าของintตัวแปรในเธรดหนึ่ง และในอีกเธรดหนึ่ง คุณอ่านค่าของตัวแปร คุณจะได้ค่าเก่าหรือค่าใหม่ นั่นคือค่าที่เกิดจากการเปลี่ยนแปลง ในเธรด 1 ไม่มี 'ค่ากลาง' อย่างไรก็ตาม สิ่งนี้ใช้ไม่ได้กับlongs และdoubles ทำไม เนื่องจากการสนับสนุนข้ามแพลตฟอร์ม จำได้ไหมว่าในระดับเริ่มต้นที่เรากล่าวว่าหลักการชี้นำของ Java คือ 'เขียนครั้งเดียว รันได้ทุกที่'? นั่นหมายถึงการสนับสนุนข้ามแพลตฟอร์ม กล่าวอีกนัยหนึ่ง แอปพลิเคชัน Java ทำงานบนแพลตฟอร์มต่างๆ ทุกประเภท ตัวอย่างเช่น บนระบบปฏิบัติการ Windows, Linux หรือ MacOS เวอร์ชันต่างๆ มันจะทำงานโดยไม่มีข้อผูกมัดกับพวกเขาทั้งหมด การชั่งน้ำหนัก 64 บิตlongdoubleเป็นพื้นฐานที่ 'หนักที่สุด' ใน 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) {

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

วิธีผลตอบแทน ()

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