-
เนื่องจากเธรดหลายเธรดของกระบวนการเดียวกันใช้พื้นที่จัดเก็บข้อมูลเดียวกัน ขณะเดียวกันก็มอบความสะดวกสบาย ก็ยังนำมาซึ่งปัญหาร้ายแรงของข้อขัดแย้งในการเข้าถึงอีกด้วย ภาษา Java มีกลไกพิเศษในการแก้ไขข้อขัดแย้งนี้ โดยป้องกันไม่ให้วัตถุข้อมูลเดียวกันถูกเข้าถึงโดยหลายเธรดในเวลาเดียวกันได้อย่างมีประสิทธิภาพ
เนื่องจากเราสามารถใช้คีย์เวิร์ดส่วนตัวเพื่อให้แน่ใจว่าวัตถุข้อมูลสามารถเข้าถึงได้โดยวิธีการเท่านั้น เราจึงต้องเสนอกลไกสำหรับวิธีการเท่านั้น กลไกนี้คือคีย์เวิร์ดที่ซิงโครไนซ์ ซึ่งมีการใช้งานสองแบบ: วิธีการซิงโครไนซ์และบล็อกซิงโครไนซ์
1. วิธีการซิงโครไนซ์: ประกาศวิธีการซิงโครไนซ์โดยการเพิ่มคำหลักที่ซิงโครไนซ์ในการประกาศวิธีการ ชอบ:
accessVal เป็นโมฆะที่ซิงโครไนซ์สาธารณะ (int newVal);
วิธีการซิงโครไนซ์จะควบคุมการเข้าถึงตัวแปรสมาชิกของคลาส: แต่ละอินสแตนซ์ของคลาสจะสอดคล้องกับการล็อค วิธีการซิงโครไนซ์แต่ละวิธีจะต้องได้รับการล็อคของอินสแตนซ์คลาสที่เรียกใช้เมธอดก่อนจึงจะสามารถดำเนินการได้ มิฉะนั้น เธรดที่เป็นสมาชิกจะถูกบล็อก เมื่อดำเนินการเมธอดแล้ว จะครอบครองตัวแปรสมาชิกของคลาสโดยเฉพาะ การล็อกจะไม่ถูกปล่อยจนกว่าจะกลับมาจากเมธอดนี้ หลังจากนั้น เธรดที่ถูกบล็อกสามารถรับการล็อกและเข้าสู่สถานะปฏิบัติการอีกครั้ง กลไกนี้ช่วยให้แน่ใจว่าสำหรับแต่ละคลาสอินสแตนซ์ในเวลาเดียวกัน ฟังก์ชันสมาชิกสูงสุดหนึ่งฟังก์ชันที่ถูกประกาศเป็นซิงโครไนซ์จะอยู่ในสถานะที่ปฏิบัติการได้ (เพราะส่วนใหญ่สามารถรับการล็อกที่สอดคล้องกับคลาสอินสแตนซ์ได้) (โปรดใส่ใจกับคำสั่งนี้ ! เนื่องจากเป็นการล็อคสำหรับคลาสอินสแตนซ์ ดังนั้นสำหรับออบเจ็กต์ในแต่ละครั้ง จะมีการดำเนินการซิงโครไนซ์เพียงวิธีเดียวเท่านั้นในแต่ละครั้ง ดังนั้นจึงหลีกเลี่ยงข้อขัดแย้งในการเข้าถึงตัวแปรสมาชิกของคลาสได้อย่างมีประสิทธิภาพ (ตราบเท่าที่วิธีการทั้งหมดที่อาจ ตัวแปรสมาชิกคลาสการเข้าถึงจะถูกประกาศเป็นแบบซิงโครไนซ์)
ใน Java ไม่เพียงแต่คลาสเท่านั้น แต่ละคลาสยังสอดคล้องกับการล็อค ด้วยวิธีนี้ เรายังสามารถประกาศฟังก์ชันสมาชิกแบบคงที่ของคลาสแบบซิงโครไนซ์เพื่อควบคุมการเข้าถึงตัวแปรสมาชิกแบบคงที่ของคลาส
ข้อบกพร่องของวิธีการซิงโครไนซ์: หากมีการประกาศวิธีการขนาดใหญ่เป็นการซิงโครไนซ์ ประสิทธิภาพจะได้รับผลกระทบอย่างมาก โดยทั่วไป หากเมธอดคลาสเธรด run() ถูกประกาศเป็นซิงโครไนซ์ วิธีการนั้นจะยังคงทำงานต่อไปตลอดวงจรชีวิตของเธรด จะทำให้การเรียกวิธีการซิงโครไนซ์ของคลาสนี้ไม่สำเร็จ แน่นอนว่าเราสามารถแก้ไขปัญหานี้ได้ด้วยการใส่โค้ดที่เข้าถึงตัวแปรสมาชิกของคลาสลงในวิธีพิเศษ ประกาศให้เป็นแบบซิงโครไนซ์ และเรียกมันในวิธีการหลัก แต่ Java ให้วิธีแก้ปัญหาที่ดีกว่าแก่เรา นั่นก็คือบล็อกซิงโครไนซ์
2. บล็อกที่ซิงโครไนซ์: ประกาศบล็อกที่ซิงโครไนซ์ผ่านคำหลักที่ซิงโครไนซ์ ไวยากรณ์มีดังนี้:
ซิงโครไนซ์ (syncObject) {
//รหัสเพื่ออนุญาตการควบคุมการเข้าถึง
-
บล็อกที่ซิงโครไนซ์คือบล็อกโค้ดที่โค้ดต้องได้รับการล็อกของอ็อบเจ็กต์ syncObject (ตามที่กล่าวไว้ก่อนหน้านี้ อาจเป็นคลาสอินสแตนซ์หรือคลาสก็ได้) ก่อนจึงจะสามารถดำเนินการได้ กลไกเฉพาะจะเหมือนกับที่กล่าวไว้ข้างต้น เนื่องจากสามารถกำหนดเป้าหมายบล็อคโค้ดใดๆ และระบุอ็อบเจ็กต์ที่ถูกล็อคได้ตามต้องการ จึงมีความยืดหยุ่นสูง
การบล็อกเธรด (การซิงโครไนซ์)
เพื่อแก้ไขข้อขัดแย้งในการเข้าถึงพื้นที่จัดเก็บข้อมูลที่ใช้ร่วมกัน Java ได้แนะนำกลไกการซิงโครไนซ์ ตอนนี้ให้เราตรวจสอบการเข้าถึงทรัพยากรที่ใช้ร่วมกันโดยหลายเธรด แน่นอนว่ากลไกการซิงโครไนซ์จะไม่เพียงพออีกต่อไป เนื่องจากทรัพยากรที่จำเป็นในเวลาใดก็ตามอาจไม่เพียงพอ พร้อม เพื่อที่จะเข้าถึงได้ ในทางกลับกัน อาจมีทรัพยากรพร้อมมากกว่าหนึ่งรายการในเวลาเดียวกัน เพื่อแก้ไขปัญหาการควบคุมการเข้าถึงในกรณีนี้ Java ได้แนะนำการสนับสนุนกลไกการบล็อก
การบล็อกหมายถึงการหยุดการทำงานของเธรดชั่วคราวเพื่อรอให้เงื่อนไขบางอย่างเกิดขึ้น (เช่น ทรัพยากรที่พร้อม) นักเรียนที่ได้ศึกษาระบบปฏิบัติการจะต้องคุ้นเคยกับมัน Java มีวิธีการมากมายเพื่อรองรับการบล็อก มาวิเคราะห์ทีละวิธีกัน
1. วิธี sleep(): sleep() อนุญาตให้คุณระบุช่วงเวลาเป็นมิลลิวินาทีเป็นพารามิเตอร์ ซึ่งจะทำให้เธรดเข้าสู่สถานะการบล็อกภายในเวลาที่กำหนดและไม่สามารถรับเวลา CPU ได้ เธรดกลับเข้าสู่สถานะปฏิบัติการอีกครั้ง
โดยทั่วไปแล้ว sleep() จะใช้เมื่อรอให้ทรัพยากรพร้อม: หลังจากการทดสอบพบว่าไม่เป็นไปตามเงื่อนไข ให้ปล่อยให้เธรดบล็อกเป็นระยะเวลาหนึ่ง จากนั้นทดสอบอีกครั้งจนกว่าจะตรงตามเงื่อนไข
2. วิธีการ Suspend() และ Resume() (ง่ายต่อการทำให้เกิดการหยุดชะงัก ล้าสมัย): ทั้งสองวิธีถูกนำมาใช้ร่วมกัน Suspend() ทำให้เธรดเข้าสู่สถานะบล็อกและจะไม่กู้คืนโดยอัตโนมัติ Resume() ที่สอดคล้องกัน เรียกว่า เธรดสามารถกลับเข้าสู่สถานะปฏิบัติการได้อีกครั้ง โดยทั่วไปแล้ว Suspend() และ Resume() จะถูกใช้เมื่อรอผลลัพธ์ที่สร้างโดยเธรดอื่น: หลังจากการทดสอบพบว่าไม่มีการสร้างผลลัพธ์ เธรดถูกบล็อก และหลังจากเธรดอื่นสร้างผลลัพธ์ ให้เรียก resume() เพื่อดำเนินการต่อ
3. วิธีการ Yield(): Yield() ทำให้เธรดสละเวลา CPU ที่จัดสรรในปัจจุบัน แต่ไม่ทำให้เธรดบล็อก นั่นคือเธรดยังอยู่ในสถานะที่สามารถเรียกใช้งานได้และอาจได้รับการจัดสรรเวลา CPU อีกครั้งที่ เวลาใดก็ได้ ผลของการเรียก Yield() เทียบเท่ากับตัวกำหนดเวลาที่ถือว่าเธรดดำเนินการเวลาเพียงพอที่จะย้ายไปยังเธรดอื่น
4. wait() และ notify() วิธีการ: ทั้งสองวิธีถูกใช้ร่วมกัน wait() ทำให้เธรดเข้าสู่สถานะการบล็อก มีสองรูปแบบ อนุญาตให้ระบุระยะเวลาเป็นมิลลิวินาทีเป็นพารามิเตอร์ และ อื่นๆ ไม่ได้
เมื่อมองแวบแรก ดูเหมือนว่าจะไม่แตกต่างจากคู่เมธอด Suspend() และ Resume() แต่จริงๆ แล้วแตกต่างกันโดยสิ้นเชิง ข้อแตกต่างหลักคือวิธีการทั้งหมดที่อธิบายไว้ข้างต้นจะไม่ปลดล็อคการล็อคที่ถูกครอบครอง (หากถูกครอบครอง) เมื่อทำการบล็อค ในขณะที่กฎที่ตรงกันข้ามนี้จะตรงกันข้าม
ความแตกต่างหลักข้างต้นนำไปสู่ชุดของความแตกต่างโดยละเอียด
ประการแรก วิธีการทั้งหมดที่อธิบายไว้ข้างต้นเป็นของคลาส Thread แต่คู่นี้เป็นของคลาส Object โดยตรง นั่นคือออบเจ็กต์ทั้งหมดมีวิธีการคู่นี้ สิ่งนี้อาจดูเหลือเชื่อในตอนแรก แต่ในความเป็นจริงแล้ว มันเป็นเรื่องปกติ เพราะเมื่อเมธอดคู่นี้บล็อก การล็อคที่ถูกครอบครองจะต้องถูกปลดล็อค และวัตถุใดๆ ก็ตามจะครอบครองการล็อค การเรียกเมธอด wait() ของวัตถุใดๆ จะทำให้เกิด เธรดที่จะบล็อกและปลดล็อควัตถุ การเรียกเมธอด notify() ของอ็อบเจ็กต์ใด ๆ จะทำให้เธรดที่เลือกแบบสุ่มถูกบล็อกโดยการเรียกเมธอด wait() ของอ็อบเจ็กต์ที่จะยกเลิกการบล็อก (แต่จะไม่ถูกดำเนินการจนกว่าจะได้รับการล็อค)
ประการที่สอง วิธีการทั้งหมดที่อธิบายไว้ข้างต้นสามารถเรียกได้ทุกที่ แต่ต้องเรียกวิธีการคู่นี้ (รอ () และแจ้ง ()) ด้วยวิธีการซิงโครไนซ์หรือบล็อก เหตุผลก็ง่ายมากเช่นกัน เฉพาะในวิธีการซิงโครไนซ์เท่านั้น หรือบล็อกเฉพาะเธรดปัจจุบันเท่านั้นที่ใช้ล็อคและสามารถปลดล็อคได้ ในทำนองเดียวกัน การล็อกบนวัตถุที่เรียกวิธีการคู่นี้ต้องเป็นของเธรดปัจจุบัน เพื่อให้สามารถนำการล็อกออกได้ ดังนั้นคู่ของการเรียกเมธอดต้องถูกวางในวิธีการซิงโครไนซ์หรือบล็อกที่มีอ็อบเจ็กต์ที่ถูกล็อกเป็นอ็อบเจ็กต์ที่เรียกคู่ของเมธอด หากไม่ตรงตามเงื่อนไขนี้ แม้ว่าโปรแกรมยังสามารถคอมไพล์ได้ ข้อยกเว้น IllegalMonitorStateException จะเกิดขึ้นขณะรันไทม์
ลักษณะข้างต้นของเมธอด wait() และ notify() กำหนดว่ามักจะใช้ร่วมกับวิธีการซิงโครไนซ์หรือบล็อก เมื่อเปรียบเทียบกับกลไกการสื่อสารระหว่างกระบวนการของระบบปฏิบัติการจะเปิดเผยความคล้ายคลึงกัน: วิธีการซิงโครไนซ์หรือบล็อกให้สิ่งที่คล้ายกัน สำหรับฟังก์ชันของระบบปฏิบัติการดั้งเดิม การดำเนินการจะไม่ถูกรบกวนโดยกลไกแบบมัลติเธรด และคู่นี้เทียบเท่ากับดั้งเดิมของบล็อกและปลุก (ทั้งสองวิธีนี้ถูกประกาศให้ซิงโครไนซ์กัน) การรวมกันของสิ่งเหล่านี้ช่วยให้เราสามารถใช้ชุดอัลกอริธึมการสื่อสารระหว่างกระบวนการที่ยอดเยี่ยมบนระบบปฏิบัติการ (เช่นอัลกอริธึมเซมาฟอร์) และสามารถใช้เพื่อแก้ปัญหาการสื่อสารระหว่างเธรดที่ซับซ้อนต่างๆ
สองประเด็นสุดท้ายเกี่ยวกับวิธีการ wait() และ notify():
ประการแรก: เธรดที่ไม่ถูกบล็อกที่เกิดจากการเรียกเมธอด notify() จะถูกสุ่มเลือกจากเธรดที่ถูกบล็อกโดยการเรียกเมธอด wait() ของอ็อบเจ็กต์ เราไม่สามารถคาดเดาได้ว่าเธรดใดจะถูกเลือก ดังนั้นควรระมัดระวังเป็นพิเศษเมื่อเขียนโปรแกรม เพื่อหลีกเลี่ยง ปัญหาที่เกิดจากความไม่แน่นอนนี้
ประการที่สอง: นอกจาก notify() แล้ว ยังมีเมธอด notifyAll() ที่สามารถมีบทบาทคล้ายกันได้ ข้อแตกต่างเพียงอย่างเดียวคือการเรียกเมธอด notifyAll() จะเป็นการลบเธรดทั้งหมดที่ถูกบล็อกโดยการเรียกเมธอด wait() ของ ปลดล็อควัตถุทั้งหมดทันที แน่นอนว่าเฉพาะเธรดที่ได้รับการล็อคเท่านั้นที่สามารถเข้าสู่สถานะปฏิบัติการได้
เมื่อพูดถึงการบล็อค เราต้องพูดถึงการหยุดชะงัก การวิเคราะห์โดยย่อสามารถเผยให้เห็นว่าทั้งเมธอด Suspend() และการเรียกเมธอด wait() โดยไม่ระบุระยะเวลาหมดเวลาอาจทำให้เกิดการชะงักงัน น่าเสียดายที่ Java ไม่รองรับการหลีกเลี่ยงการหยุดชะงักในระดับภาษา และเราต้องระมัดระวังเพื่อหลีกเลี่ยงการหยุดชะงักในการเขียนโปรแกรม
ข้างต้นเราได้วิเคราะห์วิธีการต่างๆ ของการบล็อกเธรดใน Java เรามุ่งเน้นไปที่วิธีการ wait() และ notify() เนื่องจากเป็นวิธีที่ทรงพลังที่สุดและยืดหยุ่นที่สุดในการใช้งาน แต่สิ่งนี้ยังนำไปสู่ประสิทธิภาพที่ต่ำกว่าและมัน มีแนวโน้มที่จะเกิดข้อผิดพลาดมากขึ้น ในการใช้งานจริงเราควรใช้วิธีการต่างๆ อย่างยืดหยุ่น เพื่อให้บรรลุเป้าหมายได้ดียิ่งขึ้น
ด้ายปีศาจ
Daemon threads เป็นเธรดชนิดพิเศษ ความแตกต่างระหว่างเธรดเหล่านั้นกับเธรดทั่วไปคือเธรดเหล่านั้นไม่ใช่ส่วนหลักของแอปพลิเคชัน เมื่อเธรดที่ไม่ใช่ daemon ทั้งหมดของแอปพลิเคชันยุติลง แม้ว่าจะยังมีเธรด daemon ที่ทำงานอยู่ก็ตาม จะยุติ ในทางกลับกัน แอปพลิเคชันจะไม่ยุติตราบใดที่ยังมีเธรดที่ไม่ใช่ daemon ทำงานอยู่ โดยทั่วไปแล้ว เธรด Daemon ใช้เพื่อให้บริการกับเธรดอื่นๆ ในเบื้องหลัง
คุณสามารถกำหนดได้ว่าเธรดเป็นเธรด daemon หรือไม่โดยการเรียกเมธอด isDaemon() หรือคุณสามารถเรียกเมธอด setDaemon() เพื่อตั้งค่าเธรดเป็น daemon thread
กลุ่มเธรด
กลุ่มเธรดเป็นแนวคิดเฉพาะของ Java ใน Java กลุ่มเธรดคืออ็อบเจ็กต์ของคลาส ThreadGroup แต่ละเธรดอยู่ในกลุ่มเธรดที่ไม่ซ้ำกัน ด้าย คุณสามารถระบุกลุ่มเธรดที่เป็นของเธรดได้โดยการเรียกตัวสร้างคลาส Thread ที่มีพารามิเตอร์ประเภท ThreadGroup หากไม่ได้ระบุไว้ เธรดจะมีค่าเริ่มต้นเป็นกลุ่มเธรดของระบบที่ชื่อ system
ใน Java กลุ่มเธรดทั้งหมดต้องถูกสร้างขึ้นอย่างชัดเจน ยกเว้นกลุ่มเธรดของระบบที่สร้างไว้ล่วงหน้า ใน Java กลุ่มเธรดแต่ละกลุ่มยกเว้นกลุ่มเธรดของระบบเป็นของกลุ่มเธรดอื่น คุณสามารถระบุกลุ่มเธรดที่เป็นสมาชิกได้เมื่อสร้างกลุ่มเธรดของระบบตามค่าดีฟอลต์ ด้วยวิธีนี้ กลุ่มเธรดทั้งหมดจะสร้างแผนผังโดยมีกลุ่มเธรดของระบบเป็นราก
Java ช่วยให้เราสามารถดำเนินการกับเธรดทั้งหมดในกลุ่มเธรดในเวลาเดียวกันได้ ตัวอย่างเช่น เราสามารถกำหนดลำดับความสำคัญของเธรดทั้งหมดในเธรดนั้นได้โดยการเรียกเมธอดที่สอดคล้องกันของกลุ่มเธรด และเรายังสามารถเริ่มหรือบล็อกเธรดทั้งหมดในนั้นได้ มัน.
บทบาทที่สำคัญอีกประการหนึ่งของกลไกกลุ่มเธรดของ Java คือความปลอดภัยของเธรด กลไกกลุ่มเธรดช่วยให้เราสามารถแยกแยะเธรดที่มีลักษณะความปลอดภัยที่แตกต่างกันผ่านการจัดกลุ่ม จัดการเธรดในกลุ่มต่าง ๆ ที่แตกต่างกัน และสนับสนุนการนำมาตรการรักษาความปลอดภัยที่ไม่เท่าเทียมกันมาใช้ผ่านโครงสร้างลำดับชั้นของกลุ่มเธรด คลาส ThreadGroup ของ Java มีวิธีการจำนวนมากเพื่ออำนวยความสะดวกให้เราดำเนินการแต่ละกลุ่มเธรดในแผนผังกลุ่มเธรดและแต่ละเธรดในกลุ่มเธรด
สถานะของเธรด ณ จุดที่กำหนดในเวลา เธรดสามารถอยู่ในสถานะเดียวเท่านั้น
ใหม่
กระทู้ที่ยังไม่ได้เริ่มจะอยู่ในสถานะนี้
วิ่งได้
เธรดที่ดำเนินการในเครื่องเสมือน Java อยู่ในสถานะนี้
ถูกบล็อก
เธรดที่ถูกบล็อกและรอการล็อคจอภาพอยู่ในสถานะนี้
ซึ่งรอคอย
เธรดที่รออย่างไม่มีกำหนดเพื่อให้เธรดอื่นดำเนินการเฉพาะเจาะจงอยู่ในสถานะนี้
สถานะเธรดของเธรดที่รอ เธรดอยู่ในสถานะรอเนื่องจากเรียกวิธีใดวิธีหนึ่งต่อไปนี้:
Object.wait โดยไม่มีค่าการหมดเวลา
Thread.join โดยไม่มีค่าการหมดเวลา
LockSupport.park
เธรดที่อยู่ในสถานะรอกำลังรอให้เธรดอื่นดำเนินการเฉพาะเจาะจง ตัวอย่างเช่น เธรดที่เรียกว่า Object.wait() บนวัตถุกำลังรอให้เธรดอื่นเรียก Object.notify() หรือ Object.notifyAll() บนวัตถุนั้น เธรดที่ชื่อว่า Thread.join() กำลังรอให้เธรดที่ระบุยุติการทำงาน
TIMED_WAITING
เธรดที่กำลังรอให้เธรดอื่นดำเนินการที่ขึ้นอยู่กับเวลารอที่ระบุอยู่ในสถานะนี้
สถานะเธรดของเธรดที่รอพร้อมเวลารอที่ระบุ เธรดอยู่ในสถานะรอตามกำหนดเวลาโดยการเรียกวิธีใดวิธีหนึ่งต่อไปนี้โดยระบุเวลารอเป็นค่าบวก:
กระทู้.นอน
Object.wait พร้อมค่าหมดเวลา
Thread.join ด้วยค่าการหมดเวลา
LockSupport.parkNanos
LockSupport.parkจนกระทั่ง
สิ้นสุดแล้ว
เธรดที่ออกอยู่ในสถานะนี้