เป้าหมายของการสื่อสารเธรดคือการเปิดใช้งานเธรดเพื่อส่งสัญญาณให้กันและกัน ในทางกลับกันการสื่อสารเธรดช่วยให้เธรดสามารถรอสัญญาณจากเธรดอื่น ๆ
การสื่อสารผ่านวัตถุที่ใช้ร่วมกัน
วิธีที่ง่ายในการส่งสัญญาณระหว่างเธรดคือการตั้งค่าสัญญาณในตัวแปรของวัตถุที่ใช้ร่วมกัน เธรด A ตั้งค่าตัวแปรสมาชิกบูลีน hasdatoprocess เป็นจริงในบล็อกการซิงโครไนซ์และเธรด B ยังอ่านตัวแปรสมาชิก HasDatoprocess ในบล็อกการซิงโครไนซ์ ตัวอย่างง่ายๆนี้ใช้วัตถุที่ถือสัญญาณและให้วิธีการตั้งค่าและตรวจสอบ:
คลาสสาธารณะ mysignal {boolean ที่ได้รับการป้องกัน hasDatatoprocess = false; Public Synchronized Boolean HasDatatoprocess () {return this.hasdatatoprocess; } โมฆะสาธารณะ sethasdatatoprocess (บูลีน hasdata) {this.hasdatatoprocess = hasdata; -เธรด A และ B ต้องได้รับการอ้างอิงถึงอินสแตนซ์ที่ใช้ร่วมกันของ mysignal สำหรับการสื่อสาร หากการอ้างอิงที่พวกเขาถือเป็นจุดที่แตกต่างกันอินสแตนซ์ mysingal ซึ่งกันและกันจะไม่สามารถตรวจจับสัญญาณของกันและกันได้ ข้อมูลที่จะประมวลผลสามารถเก็บไว้ในพื้นที่แคชที่ใช้ร่วมกันซึ่งจัดเก็บแยกต่างหากจากอินสแตนซ์ mysignal
รอยุ่ง
เธรด B ที่กำลังเตรียมการประมวลผลข้อมูลกำลังรอข้อมูลที่พร้อมใช้งาน กล่าวอีกนัยหนึ่งมันกำลังรอสัญญาณจากเธรด A ซึ่งทำให้ hasdatoprocess () กลับมาเป็นจริง เธรด B ทำงานในวงเพื่อรอสัญญาณนี้:
ได้รับการปกป้อง mysignal sharedSignal = ...... ในขณะที่ (! SharedSignal.hasDatatoprocess ()) {// ไม่ทำอะไรเลย ... ไม่ว่างรอ}รอ (), แจ้ง () และแจ้งเตือน ()
การรอคอยไม่ว่างไม่ได้ใช้ CPU ที่ใช้ด้ายรออย่างมีประสิทธิภาพเว้นแต่ว่าเวลารอคอยเฉลี่ยจะสั้นมาก มิฉะนั้นจะฉลาดกว่าที่จะทำให้ด้ายรอนอนหรือไม่วิ่งจนกว่าจะได้รับสัญญาณที่รออยู่
Java มีกลไกการรอคอยในตัวเพื่อให้เธรดไม่ทำงานขณะรอสัญญาณ คลาส java.lang.Object กำหนดสามวิธีรอ (), แจ้ง () และแจ้งเตือน () เพื่อใช้กลไกการรอคอยนี้
เมื่อเธรดเรียกวิธีการรอ () ของวัตถุใด ๆ มันจะกลายเป็นสถานะที่ไม่รันจนกว่าเธรดอื่นจะเรียกวิธีการแจ้งเตือน () ของวัตถุเดียวกัน ในการโทรหา () หรือแจ้ง () เธรดจะต้องได้รับการล็อคของวัตถุนั้นก่อน นั่นคือเธรดจะต้องโทรหา () หรือแจ้ง () ในบล็อกการซิงโครไนซ์ ต่อไปนี้เป็นเวอร์ชันที่แก้ไขของ mysingal - mywaitnotify โดยใช้การรอ () และแจ้ง ()::
Public Class MonitorObject {} คลาสสาธารณะ MyWaitNotify {MonitorObject myMonitorObject = new MonitorObject (); โมฆะสาธารณะ dowait () {ซิงโครไนซ์ (myMonitorObject) {ลอง {myMonitorObject.wait (); } catch (interruptedException e) {... }}} โมฆะสาธารณะ donotify () {ซิงโครไนซ์ (myMonitorObject) {myMonitorObject.notify (); -เธรดที่รอจะเรียก dowait () ในขณะที่เธรดปลุกจะเรียก donotify () เมื่อเธรดเรียกใช้วิธีการแจ้งเตือน () ของวัตถุหนึ่งในเธรดที่รอวัตถุจะถูกปลุกและได้รับอนุญาตให้ดำเนินการ (หมายเหตุ: เธรดนี้จะถูกปลุกให้ตื่นขึ้นมาจะสุ่มและไม่สามารถระบุได้ว่าเธรดที่จะตื่นขึ้นมา) นอกจากนี้ยังมีวิธีการแจ้งเตือน () เพื่อปลุกเธรดทั้งหมดที่รอวัตถุที่กำหนด
อย่างที่คุณเห็นไม่ว่าจะเป็นเธรดรอหรือเธรดปลุกมันเรียกว่ารอ () และแจ้ง () ในบล็อกการซิงโครไนซ์ นี่คือข้อบังคับ! หากเธรดไม่ถือการล็อควัตถุจะไม่สามารถโทรรอได้ (), แจ้ง () หรือแจ้งเตือน () มิฉะนั้นจะมีการโยนข้อยกเว้นที่ผิดกฎหมาย MonmonitorStateException
(หมายเหตุ: นี่คือวิธีการใช้งาน JVM เมื่อคุณโทรหารอจะตรวจสอบก่อนว่าเธรดปัจจุบันเป็นเจ้าของล็อคหรือไม่
แต่เป็นไปได้อย่างไร? เมื่อรอให้เธรดดำเนินการในบล็อกการซิงโครไนซ์มันจะไม่ถือล็อคของวัตถุจอภาพเสมอ (วัตถุ MyMonitor) หรือไม่? เธรดที่รอสามารถบล็อกการปลุกเข้าสู่บล็อกซิงโครนัสของ donotify () ได้หรือไม่? คำตอบคือ: มันเป็นเรื่องจริง เมื่อเธรดเรียกวิธีการรอ () มันจะปล่อยล็อคบนวัตถุตรวจสอบที่จัดขึ้น สิ่งนี้จะช่วยให้เธรดอื่นโทรรอ () หรือแจ้ง () เช่นกัน
เมื่อเธรดถูกปลุกขึ้นมาจะไม่สามารถออกจากเมธอดการโทร () ได้ทันทีจนกว่าจะมีการแจ้งเตือน ()
คลาสสาธารณะ mywaitNotify2 {MonitorObject myMonitorObject = new MonitorObject (); บูลีน wassignaled = false; โมฆะสาธารณะ dowait () {ซิงโครไนซ์ (myMonitorObject) {ถ้า (! wassignaled) {ลอง {myMonitorObject.wait (); } catch (interruptedException e) {... }} // สัญญาณที่ชัดเจนและทำงานต่อ Wassignaled = false; }} โมฆะสาธารณะ donotify () {ซิงโครไนซ์ (myMonitorObject) {wassignaled = true; MyMonitorObject.Notify (); -
เธรดออกจากบล็อกการซิงโครไนซ์ของตัวเอง กล่าวอีกนัยหนึ่งเธรดที่ตื่นจะต้องกลับมาที่ล็อคของวัตถุจอภาพก่อนที่จะออกจากการเรียกใช้วิธีการรอ () เนื่องจากการเรียกใช้วิธีการรอทำงานในบล็อกการซิงโครไนซ์ หากมีหลายเธรดที่ถูกปลุกด้วย NotifyAll () ดังนั้นในเวลาเดียวกันมีเพียงเธรดเดียวเท่านั้นที่สามารถออกจากวิธีการรอ () เนื่องจากแต่ละเธรดจะต้องได้รับการล็อคของวัตถุจอภาพก่อนที่จะออกจากการรอ ()
สัญญาณที่ไม่ได้รับ
วิธีการแจ้งเตือน () และ NotifyAll () ไม่ได้บันทึกวิธีที่เรียกพวกเขาเพราะเมื่อมีการเรียกวิธีการทั้งสองนี้เป็นไปได้ว่าไม่มีเธรดอยู่ในสถานะรอ สัญญาณการแจ้งเตือนถูกยกเลิก ดังนั้นหากเธรดโทรแจ้ง () ก่อนที่จะได้รับการแจ้งเตือนก่อนโทรรอ () เธรดรอจะพลาดสัญญาณนี้ นี่อาจเป็นหรือไม่มีปัญหา อย่างไรก็ตามในบางกรณีสิ่งนี้อาจทำให้เธรดรอรอเสมอและไม่ตื่นขึ้นมาอีกต่อไปเพราะเธรดพลาดสัญญาณปลุก
เพื่อหลีกเลี่ยงการสูญเสียสัญญาณพวกเขาจะต้องถูกบันทึกไว้ในคลาสสัญญาณ ในตัวอย่าง mywaitnotify สัญญาณการแจ้งเตือนควรเก็บไว้ในตัวแปรสมาชิกของอินสแตนซ์ mywaitnotify นี่คือ MyWaitNotify เวอร์ชันที่แก้ไขแล้ว:
คลาสสาธารณะ mywaitNotify2 {MonitorObject myMonitorObject = new MonitorObject (); บูลีน wassignaled = false; โมฆะสาธารณะ dowait () {ซิงโครไนซ์ (myMonitorObject) {ถ้า (! wassignaled) {ลอง {myMonitorObject.wait (); } catch (interruptedException e) {... }} // สัญญาณที่ชัดเจนและทำงานต่อ Wassignaled = false; }} โมฆะสาธารณะ donotify () {ซิงโครไนซ์ (myMonitorObject) {wassignaled = true; MyMonitorObject.Notify (); -โปรดทราบว่าเมธอด donotify () ตั้งค่าตัวแปร Wassignalled เป็น TRUE ก่อนที่จะเรียกการแจ้งเตือน () ในเวลาเดียวกันโปรดทราบว่าเมธอด dowait () จะตรวจสอบตัวแปร Wassignaled ก่อนโทรรอ () ในความเป็นจริงหากไม่ได้รับสัญญาณในช่วงระยะเวลาระหว่างการโทร dowait () ก่อนหน้าและการโทร dowait () นี้จะโทรรอเท่านั้น ()
(หมายเหตุหลักฐาน: เพื่อหลีกเลี่ยงการสูญเสียสัญญาณให้ใช้ตัวแปรเพื่อบันทึกว่าได้รับการแจ้งเตือนก่อนที่จะแจ้งให้ทราบให้ตั้งค่าตัวเองให้ได้รับแจ้งหลังจากรอให้ตั้งค่าตัวเองให้ไม่ได้รับแจ้งและต้องรอการแจ้งเตือน)
ปลุกปลอมขึ้น
ด้วยเหตุผลบางอย่างเป็นไปได้ที่เธรดจะตื่นขึ้นโดยไม่ต้องโทรแจ้ง () และแจ้งเตือน () สิ่งนี้เรียกว่า Wakeups ปลอม ตื่นขึ้นมาโดยไม่มีเหตุผล
หากการปลุกที่ผิดพลาดเกิดขึ้นในวิธี dowait () ของ mywaitnotify2 เธรดรอสามารถดำเนินการต่อไปได้แม้ว่าจะไม่ได้รับสัญญาณที่ถูกต้องก็ตาม ซึ่งอาจทำให้เกิดปัญหาร้ายแรงกับใบสมัครของคุณ
เพื่อป้องกันการปลุกที่ผิดพลาดตัวแปรสมาชิกที่ถือสัญญาณจะถูกตรวจสอบในช่วงเวลาหนึ่งแทนที่จะอยู่ในนิพจน์ IF ในขณะที่ลูปเรียกว่าสปินล็อค (หมายเหตุ: วิธีการนี้ควรระมัดระวังการใช้งาน JVM ในปัจจุบันสปินใช้ CPU หากวิธีการ donotify ไม่ได้เรียกใช้เป็นเวลานานวิธีการ Dowait จะหมุนอย่างต่อเนื่องและ CPU จะใช้มากเกินไป) ด้ายที่ถูกปลุกจะหมุนจนกว่าเงื่อนไขในการล็อคสปิน (ในขณะที่ลูป) กลายเป็นเท็จ MyWaitNotify รุ่นที่แก้ไขต่อไปนี้แสดงสิ่งนี้:
คลาสสาธารณะ mywaitNotify3 {MonitorObject myMonitorObject = new MonitorObject (); บูลีน wassignaled = false; โมฆะสาธารณะ dowait () {ซิงโครไนซ์ (myMonitorObject) {ในขณะที่ (! wassignaled) {ลอง {myMonitorObject.wait (); } catch (interruptedException e) {... }} // สัญญาณที่ชัดเจนและทำงานต่อ Wassignaled = false; }} โมฆะสาธารณะ donotify () {ซิงโครไนซ์ (myMonitorObject) {wassignaled = true; MyMonitorObject.Notify (); -โปรดทราบว่าวิธีการรอ () อยู่ในขณะที่ลูปไม่ใช่ในนิพจน์ IF หากเธรดรอตื่นขึ้นมาโดยไม่ได้รับสัญญาณตัวแปร Wassignaled จะกลายเป็นเท็จและในขณะที่ลูปจะถูกดำเนินการอีกครั้งพร้อมแจ้งเธรดปลุกให้กลับไปที่สถานะการรอคอย
หลายเธรดกำลังรอสัญญาณเดียวกัน
หากคุณมีหลายเธรดที่รอคอยและถูกปลุกให้ตื่นขึ้นโดย NotifyAll () แต่มีเพียงหนึ่งเดียวเท่านั้นที่ได้รับอนุญาตให้ดำเนินการต่อไปโดยใช้การวนซ้ำในขณะที่เป็นวิธีที่ดีเช่นกัน มีเพียงหนึ่งเธรดเท่านั้นที่สามารถล็อควัตถุตรวจสอบได้ในแต่ละครั้งซึ่งหมายความว่ามีเพียงเธรดเดียวเท่านั้นที่สามารถออกจากการโทร () การโทรและล้างค่าสถานะ Wassignaled (ตั้งค่าเป็นเท็จ) เมื่อเธรดนี้ออกจากบล็อกการซิงโครไนซ์ dowait () เธรดอื่น ๆ จะออกจากการโทร () และตรวจสอบค่าตัวแปรที่เป็น wassignaled ในขณะที่ลูป อย่างไรก็ตามธงนี้ได้รับการเคลียร์โดยด้ายที่ตื่นขึ้นมาครั้งแรกดังนั้นส่วนที่เหลือของเธรดที่ถูกปลุกจะกลับไปที่สถานะการรอคอยจนกว่าจะถึงครั้งต่อไปที่สัญญาณมาถึง
อย่าโทรรอ () ในค่าคงที่สตริงหรือวัตถุส่วนกลาง
(การพิสูจน์หมายเหตุ: ค่าคงที่สตริงที่กล่าวถึงในบทนี้หมายถึงตัวแปรที่มีค่าคงที่)
เวอร์ชันก่อนหน้าของบทความนี้ใช้ค่าคงที่สตริง ("") เป็นวัตถุท่อในตัวอย่าง MyWaitNotify นี่คือตัวอย่าง:
คลาสสาธารณะ mywaitNotify {String myMonitorObject = ""; บูลีน wassignaled = false; โมฆะสาธารณะ dowait () {ซิงโครไนซ์ (myMonitorObject) {ในขณะที่ (! wassignaled) {ลอง {myMonitorObject.wait (); } catch (interruptedException e) {... }} // สัญญาณที่ชัดเจนและทำงานต่อ Wassignaled = false; }} โมฆะสาธารณะ donotify () {ซิงโครไนซ์ (myMonitorObject) {wassignaled = true; MyMonitorObject.Notify (); -ปัญหาที่เกิดจากการโทรรอ () และแจ้ง () ในบล็อกการซิงโครไนซ์ของสตริงว่างเป็นล็อค (หรือสตริงคงที่อื่น ๆ ) คือ JVM/คอมไพเลอร์จะแปลงสตริงคงที่เป็นวัตถุเดียวกัน ซึ่งหมายความว่าแม้ว่าคุณจะมีอินสแตนซ์ mywaitnotify ที่แตกต่างกัน 2 อินสแตนซ์พวกเขาทั้งหมดอ้างถึงอินสแตนซ์สตริงว่างเปล่าเดียวกัน นอกจากนี้ยังหมายความว่ามีความเสี่ยงที่เธรดที่เรียกว่า dowait () ในอินสแตนซ์ mywaitnotify แรกจะถูกปลุกโดยเธรดที่เรียกว่า donotify () ในอินสแตนซ์ mywaitnotify ที่สอง สถานการณ์นี้สามารถวาดได้ดังนี้:
ตอนแรกนี่อาจไม่ใช่ปัญหาใหญ่ ท้ายที่สุดถ้า donotify () ถูกเรียกใช้อินสแตนซ์ mywaitnotify ที่สองสิ่งที่เกิดขึ้นจริง ๆ คือเธรด A และ B ถูกปลุกให้ตื่นขึ้นอย่างผิด ๆ เธรด Wake-Up (A หรือ B) จะตรวจสอบค่าสัญญาณในขณะที่ลูปจากนั้นกลับไปที่สถานะการรอเนื่องจาก donotify () ไม่ได้ถูกเรียกใช้อินสแตนซ์ MyWaitNotify แรกและนี่คืออินสแตนซ์ที่รออยู่ สถานการณ์นี้เทียบเท่ากับการกระตุ้นการตื่นขึ้นมาผิด เธรด A หรือ B ตื่นขึ้นโดยไม่ต้องอัปเดตค่าสัญญาณ แต่รหัสจัดการกับสถานการณ์นี้ดังนั้นเธรดจึงกลับไปที่สถานะการรอคอย โปรดจำไว้ว่าแม้ว่า 4 เธรดการโทรรอ () และแจ้ง () บนอินสแตนซ์สตริงที่ใช้ร่วมกันเดียวกันสัญญาณใน dowait () และ donotify () จะถูกบันทึกโดย 2 mywaitnotify อินสแตนซ์ตามลำดับ การโทร Donotify () MyWaitNotify1 อาจปลุกเธรดของ MyWaitNotify2 แต่ค่าสัญญาณจะถูกบันทึกไว้ใน MyWaitNotify1 เท่านั้น
ปัญหาคือว่าเนื่องจาก donotify () การโทรแจ้งเตือนเท่านั้น () แทนการแจ้งเตือน () แม้ว่าจะมี 4 เธรดที่รออยู่ในอินสแตนซ์สตริง (สตริงว่างเปล่า) เดียวกันมีเพียงเธรดเดียวเท่านั้นที่ถูกปลุก ดังนั้นหากเธรด A หรือ B ถูกปลุกด้วยสัญญาณที่ส่งไปยัง C หรือ D มันจะตรวจสอบค่าสัญญาณของตัวเองเพื่อดูว่าได้รับสัญญาณใด ๆ หรือไม่จากนั้นกลับไปที่สถานะการรอคอย ทั้ง C และ D ไม่ถูกปลุกให้ตรวจสอบค่าสัญญาณที่พวกเขาได้รับจริงดังนั้นสัญญาณก็หายไป สถานการณ์นี้เทียบเท่ากับปัญหาของสัญญาณที่หายไปดังกล่าวข้างต้น C และ D ถูกส่งไปยังสัญญาณ แต่ไม่สามารถตอบสนองต่อสัญญาณได้
หากเมธอด donotify () โทรแจ้งให้ทราบ () แทนการแจ้งเตือน () เธรดที่รอทั้งหมดจะถูกปลุกและค่าสัญญาณจะถูกตรวจสอบในทางกลับกัน เธรด A และ B จะกลับไปที่สถานะการรอ แต่มีเพียงหนึ่งเธรดใน C หรือ D สังเกตสัญญาณและออกจากการโทรเมธอด dowait () อื่น ๆ ใน C หรือ D จะกลับไปที่สถานะการรอเนื่องจากเธรดที่ได้รับสัญญาณจะล้างค่าสัญญาณ (ตั้งค่าเป็นเท็จ) ในระหว่างกระบวนการออกจาก dowait ()
หลังจากอ่านย่อหน้าข้างต้นคุณอาจพยายามใช้ NotifyAll () แทนการแจ้งเตือน () แต่นี่เป็นความคิดที่ไม่ดีตามประสิทธิภาพ เมื่อเธรดเดียวเท่านั้นที่สามารถตอบสนองต่อสัญญาณได้ไม่มีเหตุผลที่จะปลุกเธรดทั้งหมดทุกครั้ง
ดังนั้น: ในกลไกการรอคอย ()/แจ้ง () อย่าใช้วัตถุส่วนกลางค่าคงที่สตริง ฯลฯ วัตถุที่ไม่ซ้ำกันที่สอดคล้องกันควรใช้ ตัวอย่างเช่นแต่ละอินสแตนซ์ของ MyWaitNotify3 มีวัตถุการตรวจสอบของตัวเองแทนการโทรรอ ()/แจ้ง () บนสตริงที่ว่างเปล่า
ข้างต้นคือข้อมูลเกี่ยวกับการสื่อสารมัลติเธรดและการสื่อสารหลายเธรด Java เราจะยังคงเพิ่มข้อมูลที่เกี่ยวข้องในอนาคต ขอบคุณสำหรับการสนับสนุนเว็บไซต์นี้!