บทความที่มา: http://www.javacodegeeks.com/2015/09/concurrency-fundamentals-deadlocks-and-object-monitors.html โพสต์โดย
Martin Mois บทความ นี้เป็นส่วนหนึ่งของ หลักสูตร Java Concurrency Fundamentals ของเรา ในหลักสูตรนี้ คุณจะเจาะลึกความมหัศจรรย์ของความเท่าเทียม คุณจะได้เรียนรู้พื้นฐานของความขนานและโค้ดคู่ขนาน และทำความคุ้นเคยกับแนวคิดต่างๆ เช่น อะตอมมิกซิตี การซิงโครไนซ์ และความปลอดภัยของเธรด ลองดูที่นี่ !
เนื้อหา
1.
ความมีชีวิตชีวา 1.1
Deadlock 1.2
ความอดอยาก 2.
อ็อบเจ็กต์มอนิเตอร์ด้วย wait() และ notify() 2.1
บล็อกซิงโครไนซ์ที่ซ้อนกันพร้อม wait() และ notify() 2.2
เงื่อนไขในบล็อก ซิงโครไนซ์ 3.
การออกแบบสำหรับมัลติเธรด 3.1
อ็อบเจ็กต์ที่ไม่เปลี่ยนรูป 3.2
การออกแบบ API 3.3
ที่เก็บเธรดในเครื่อง
เมื่อพัฒนาแอปพลิเคชันที่ใช้การทำงานแบบขนานเพื่อให้บรรลุเป้าหมาย คุณอาจพบสถานการณ์ที่เธรดที่แตกต่างกันสามารถบล็อกซึ่งกันและกันได้ หากแอปพลิเคชันทำงานช้ากว่าที่คาดไว้ในสถานการณ์นี้ เราจะบอกว่าแอปพลิเคชันไม่ทำงานตามที่คาดไว้ ในส่วนนี้ เราจะมาดูปัญหาที่อาจคุกคามความอยู่รอดของแอปพลิเคชันแบบมัลติเธรดโดยละเอียดยิ่งขึ้น
คำว่า deadlock เป็นที่รู้จักกันดีในหมู่นักพัฒนาซอฟต์แวร์และแม้กระทั่งผู้ใช้ทั่วไปส่วนใหญ่ก็ใช้คำนี้เป็นครั้งคราว แม้ว่าจะไม่ใช่ความหมายที่ถูกต้องเสมอไปก็ตาม หากพูดอย่างเคร่งครัด คำนี้หมายความว่าแต่ละเธรดจากสองเธรด (หรือมากกว่า) กำลังรอให้เธรดอื่นปล่อยทรัพยากรที่ถูกล็อกโดยเธรดนั้น ในขณะที่เธรดแรกเองได้ล็อกทรัพยากรที่เธรดที่สองกำลังรอการเข้าถึง: เพื่อให้เข้าใจดีขึ้น ปัญหา ลองดูที่
Thread 1: locks resource A, waits for resource B Thread 2: locks resource B, waits for resource A
รหัสต่อไปนี้:
public class Deadlock implements Runnable { private static final Object resource1 = new Object(); private static final Object resource2 = new Object(); private final Random random = new Random(System.currentTimeMillis()); public static void main(String[] args) { Thread myThread1 = new Thread(new Deadlock(), "thread-1"); Thread myThread2 = new Thread(new Deadlock(), "thread-2"); myThread1.start(); myThread2.start(); } public void run() { for (int i = 0; i < 10000; i++) { boolean b = random.nextBoolean(); if (b) { System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 1."); synchronized (resource1) { System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 1."); System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 2."); synchronized (resource2) { System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 2."); } } } else { System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 2."); synchronized (resource2) { System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 2."); System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 1."); synchronized (resource1) { System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 1."); } } } } } }
ดังที่คุณเห็นจากโค้ดด้านบน เธรดสองตัวเริ่มต้นและพยายามล็อกทรัพยากรคงที่สองตัว แต่สำหรับการล็อคตาย เราจำเป็นต้องมีลำดับที่แตกต่างกันสำหรับทั้งสองเธรด ดังนั้นเราจึงใช้อินสแตนซ์ของอ็อบเจ็กต์ Random เพื่อเลือกทรัพยากรที่เธรดต้องการล็อคก่อน ถ้าตัวแปรบูลีน b เป็นจริง ดังนั้นทรัพยากร1จะถูกล็อคก่อน จากนั้นเธรดจะพยายามรับการล็อคสำหรับทรัพยากร2 ถ้า b เป็นเท็จ แสดงว่าเธรดจะล็อกทรัพยากร2 จากนั้นพยายามรับทรัพยากร1 โปรแกรมนี้ไม่จำเป็นต้องรันเป็นเวลานานเพื่อให้เกิดการหยุดชะงักครั้งแรก เช่น โปรแกรมจะหยุดทำงานตลอดไปหากเราไม่ขัดจังหวะ:
[thread-1] Trying to lock resource 1. [thread-1] Locked resource 1. [thread-1] Trying to lock resource 2. [thread-1] Locked resource 2. [thread-2] Trying to lock resource 1. [thread-2] Locked resource 1. [thread-1] Trying to lock resource 2. [thread-1] Locked resource 2. [thread-2] Trying to lock resource 2. [thread-1] Trying to lock resource 1.
ในการรันนี้ tread-1 ได้รับการล็อคทรัพยากร 2 และกำลังรอการล็อคของทรัพยากร 1 ในขณะที่ดอกยาง-2 มีการล็อคทรัพยากร 1 และกำลังรอทรัพยากร 2 หากเราต้องตั้งค่าของตัวแปรบูลีน b ในโค้ดด้านบนให้เป็นจริง เราจะไม่สามารถสังเกตเห็นการหยุดชะงักใดๆ ได้เนื่องจากลำดับที่การร้องขอการล็อกเธรด-1 และเธรด-2 จะเหมือนกันเสมอ ในสถานการณ์นี้ หนึ่งในสองเธรดจะได้รับการล็อคก่อน จากนั้นจึงร้องขอวินาที ซึ่งยังคงพร้อมใช้งานเนื่องจากเธรดอื่นกำลังรอการล็อกครั้งแรก โดยทั่วไป เราสามารถแยกแยะเงื่อนไขที่จำเป็นต่อไปนี้เพื่อให้เกิดการชะงักงันได้: - การดำเนินการที่ใช้ร่วมกัน: มีทรัพยากรที่สามารถเข้าถึงได้โดยเธรดเดียวเท่านั้นได้ตลอดเวลา - การระงับทรัพยากร: ในขณะที่ได้รับทรัพยากรหนึ่ง เธรดจะพยายามรับการล็อคอีกครั้งในทรัพยากรที่ไม่ซ้ำกันบางอย่าง - ไม่มีการสำรอง: ไม่มีกลไกในการปล่อยทรัพยากรหากมีเธรดหนึ่งล็อคไว้ในช่วงระยะเวลาหนึ่ง - การรอแบบวงกลม: ในระหว่างการดำเนินการ ชุดของเธรดจะเกิดขึ้นโดยที่เธรดสองเธรด (หรือมากกว่า) รอซึ่งกันและกันเพื่อปล่อยทรัพยากรที่ถูกล็อค แม้ว่ารายการเงื่อนไขจะดูยาว แต่ก็ไม่ใช่เรื่องแปลกที่แอปพลิเคชันแบบมัลติเธรดที่ใช้งานได้ดีจะมีปัญหาการหยุดชะงัก แต่คุณสามารถป้องกันได้หากคุณสามารถลบเงื่อนไขข้อใดข้อหนึ่งข้างต้นได้: - การดำเนินการที่ใช้ร่วมกัน: เงื่อนไขนี้มักจะไม่สามารถลบออกได้เมื่อต้องใช้ทรัพยากรโดยบุคคลเพียงคนเดียวเท่านั้น แต่นี่ไม่จำเป็นต้องเป็นเหตุผล เมื่อใช้ระบบ DBMS วิธีแก้ไขที่เป็นไปได้ แทนที่จะใช้การล็อกในแง่ร้ายกับแถวตารางบางแถวที่จำเป็น ต้องอัปเดต คือการใช้เทคนิคที่เรียกว่า
Optimistic Locking - วิธีหลีกเลี่ยงการถือครองทรัพยากรในขณะที่รอทรัพยากรพิเศษอื่นคือการล็อคทรัพยากรที่จำเป็นทั้งหมดที่จุดเริ่มต้นของอัลกอริทึม และปล่อยทรัพยากรทั้งหมดหากเป็นไปไม่ได้ที่จะล็อคทรัพยากรทั้งหมดในคราวเดียว แน่นอนว่าสิ่งนี้ไม่สามารถทำได้เสมอไปบางทีอาจไม่ทราบทรัพยากรที่ต้องมีการล็อคล่วงหน้าหรือแนวทางนี้อาจนำไปสู่การสิ้นเปลืองทรัพยากร - หากไม่สามารถรับการล็อคได้ในทันที วิธีเลี่ยงการหยุดชะงักที่เป็นไปได้คือการเริ่มการหมดเวลา ตัวอย่างเช่น คลาส
ReentrantLockจาก SDK ให้ความสามารถในการกำหนดวันหมดอายุสำหรับการล็อค - ดังที่เราเห็นจากตัวอย่างข้างต้น การชะงักงันจะไม่เกิดขึ้นหากลำดับของการร้องขอไม่แตกต่างกันในเธรดที่ต่างกัน วิธีนี้ควบคุมได้ง่ายหากคุณสามารถใส่โค้ดการบล็อกทั้งหมดไว้ในวิธีเดียวที่เธรดทั้งหมดต้องผ่าน ในแอปพลิเคชันขั้นสูง คุณอาจพิจารณาใช้ระบบตรวจจับการหยุดชะงักด้วยซ้ำ ที่นี่ คุณจะต้องใช้ลักษณะคล้ายการตรวจสอบเธรด ซึ่งแต่ละเธรดจะรายงานว่าได้รับการล็อคสำเร็จแล้ว และกำลังพยายามรับการล็อค หากเธรดและการล็อกถูกจำลองเป็นกราฟกำกับ คุณสามารถตรวจพบได้ว่าเมื่อเธรดที่ต่างกันสองเธรดเก็บทรัพยากรไว้ในขณะที่พยายามเข้าถึงทรัพยากรที่ถูกล็อกอื่น ๆ ในเวลาเดียวกัน หากคุณสามารถบังคับให้เธรดที่บล็อกปล่อยทรัพยากรที่จำเป็นได้ คุณสามารถแก้ไขสถานการณ์การชะงักงันได้โดยอัตโนมัติ
ตัวกำหนดตารางเวลาตัดสินใจว่าควรดำเนินการ เธรดใด
ในสถานะ RUNNABLE ต่อไป การตัดสินใจจะขึ้นอยู่กับลำดับความสำคัญของเธรด ดังนั้นเธรดที่มีลำดับความสำคัญต่ำกว่าจะได้รับเวลา CPU น้อยกว่าเมื่อเปรียบเทียบกับเธรดที่มีลำดับความสำคัญสูงกว่า สิ่งที่ดูเหมือนเป็นวิธีแก้ปัญหาที่สมเหตุสมผลอาจทำให้เกิดปัญหาได้หากถูกละเมิด หากเธรดที่มีลำดับความสำคัญสูงดำเนินการอยู่เกือบตลอดเวลา ดูเหมือนว่าเธรดที่มีลำดับความสำคัญต่ำจะขาดแคลนเนื่องจากไม่มีเวลาเพียงพอในการทำงานอย่างถูกต้อง ดังนั้นจึงขอแนะนำให้ตั้งค่าลำดับความสำคัญของเธรดเฉพาะเมื่อมีเหตุผลที่น่าสนใจเท่านั้น ตัวอย่างที่ไม่ชัดเจนของการอดอาหารของเธรดจะได้รับ เช่น โดยเมธอด Finalize() เป็นวิธีสำหรับภาษา Java ในการรันโค้ดก่อนที่อ็อบเจ็กต์จะถูกรวบรวมแบบขยะ แต่ถ้าคุณดูที่ลำดับความสำคัญของเธรดที่กำลังปิดท้าย คุณจะสังเกตเห็นว่าเธรดนั้นไม่ได้ทำงานด้วยลำดับความสำคัญสูงสุด ผลที่ตามมา การอดอาหารของเธรดเกิดขึ้นเมื่อเมธอด Finalize() ของอ็อบเจ็กต์ของคุณใช้เวลามากเกินไปเมื่อเทียบกับส่วนที่เหลือของโค้ด ปัญหาอีกประการหนึ่งเกี่ยวกับเวลาดำเนินการเกิดขึ้นจากข้อเท็จจริงที่ว่าไม่ได้กำหนดไว้ในลำดับที่เธรดจะข้ามบล็อกที่ซิงโครไนซ์ เมื่อเธรดแบบขนานจำนวนมากกำลังสำรวจโค้ดบางส่วนที่ถูกเฟรมในบล็อกที่ซิงโครไนซ์ อาจเกิดขึ้นได้ว่าบางเธรดต้องรอนานกว่าเธรดอื่นก่อนที่จะเข้าสู่บล็อก ตามทฤษฎีแล้ว พวกเขาอาจไม่เคยไปถึงที่นั่นเลย วิธีแก้ปัญหานี้เรียกว่าการบล็อกที่ "ยุติธรรม" การล็อคที่ยุติธรรมจะคำนึงถึงเวลารอของเธรดเมื่อพิจารณาว่าใครจะผ่านเป็นคนต่อไป ตัวอย่างการใช้งานการล็อคโดยชอบธรรมมีอยู่ใน Java SDK: java.util.concurrent.locks.ReentrantLock หากใช้คอนสตรัคเตอร์โดยตั้งค่าสถานะบูลีนเป็นจริง ReentrantLock จะให้สิทธิ์เข้าถึงเธรดที่รอนานที่สุด สิ่งนี้รับประกันว่าจะไม่มีความหิวโหย แต่ในขณะเดียวกันก็นำไปสู่ปัญหาการเพิกเฉยต่อลำดับความสำคัญ ด้วยเหตุนี้ กระบวนการที่มีลำดับความสำคัญต่ำกว่าซึ่งมักจะรออยู่ที่อุปสรรคนี้อาจทำงานบ่อยขึ้น สุดท้ายแต่ไม่ท้ายสุด คลาส ReentrantLock สามารถพิจารณาเฉพาะเธรดที่กำลังรอการล็อคเท่านั้น เช่น กระทู้ที่เปิดตัวบ่อยเพียงพอและถึงอุปสรรค หากลำดับความสำคัญของเธรดต่ำเกินไป สิ่งนี้จะไม่เกิดขึ้นบ่อยนัก ดังนั้นเธรดที่มีลำดับความสำคัญสูงจะยังคงผ่านการล็อกบ่อยกว่า
ในการประมวลผลแบบมัลติเธรด สถานการณ์ทั่วไปคือการมีเธรดผู้ปฏิบัติงานรอให้ผู้ผลิตสร้างงานบางอย่างให้พวกเขา แต่อย่างที่เราได้เรียนรู้ การรอคอยอย่างแข็งขันในขณะที่ตรวจสอบค่าที่แน่นอนไม่ใช่ตัวเลือกที่ดีในแง่ของเวลา CPU การใช้เมธอด Thread.sleep() ในสถานการณ์นี้ก็ไม่เหมาะสมเช่นกันหากเราต้องการเริ่มงานทันทีหลังจากมาถึง เพื่อจุดประสงค์นี้ ภาษาการเขียนโปรแกรม Java มีโครงสร้างอื่นที่สามารถใช้ในรูปแบบนี้ได้: wait() และ notify() เมธอด wait() ซึ่งสืบทอดมาจากอ็อบเจ็กต์ทั้งหมดจากคลาส java.lang.Object สามารถใช้เพื่อระงับเธรดปัจจุบันและรอจนกว่าเธรดอื่นจะปลุกเราให้ตื่นโดยใช้เมธอด notify() เพื่อให้ทำงานได้อย่างถูกต้อง เธรดที่เรียกใช้เมธอด wait() จะต้องล็อคที่ได้มาก่อนหน้านี้โดยใช้คีย์เวิร์ดที่ซิงโครไนซ์ เมื่อเรียก wait() การล็อคจะถูกปล่อย และเธรดจะรอจนกระทั่งเธรดอื่นที่เก็บการเรียกล็อคไว้จะแจ้งเตือน() บนอินสแตนซ์ของวัตถุเดียวกัน ในแอปพลิเคชันแบบมัลติเธรด อาจมีเธรดมากกว่าหนึ่งเธรดที่รอการแจ้งเตือนบนออบเจ็กต์บางอย่างโดยธรรมชาติ ดังนั้นจึงมีสองวิธีในการปลุกเธรดที่แตกต่างกัน: notify() และ notifyAll() ในขณะที่วิธีแรกปลุกหนึ่งในเธรดที่รออยู่ เมธอด notifyAll() จะปลุกเธรดทั้งหมด แต่โปรดทราบว่า เช่นเดียวกับคีย์เวิร์ดที่ซิงโครไนซ์ ไม่มีกฎเกณฑ์ที่กำหนดว่าเธรดใดที่จะถูกปลุกครั้งต่อไปเมื่อมีการเรียกใช้ notify() ในตัวอย่างง่ายๆ กับผู้ผลิตและผู้บริโภค สิ่งนี้ไม่สำคัญ เนื่องจากเราไม่สนใจว่าเธรดใดจะถูกปลุก รหัสต่อไปนี้แสดงให้เห็นว่า wait() และ notify() สามารถใช้เพื่อทำให้คอนซูเมอร์เธรดต้องรองานใหม่เข้าคิวโดยเธรดผู้ผลิต:
package a2; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; public class ConsumerProducer { private static final Queue queue = new ConcurrentLinkedQueue(); private static final long startMillis = System.currentTimeMillis(); public static class Consumer implements Runnable { public void run() { while (System.currentTimeMillis() < (startMillis + 10000)) { synchronized (queue) { try { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if (!queue.isEmpty()) { Integer integer = queue.poll(); System.out.println("[" + Thread.currentThread().getName() + "]: " + integer); } } } } public static class Producer implements Runnable { public void run() { int i = 0; while (System.currentTimeMillis() < (startMillis + 10000)) { queue.add(i++); synchronized (queue) { queue.notify(); } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (queue) { queue.notifyAll(); } } } public static void main(String[] args) throws InterruptedException { Thread[] consumerThreads = new Thread[5]; for (int i = 0; i < consumerThreads.length; i++) { consumerThreads[i] = new Thread(new Consumer(), "consumer-" + i); consumerThreads[i].start(); } Thread producerThread = new Thread(new Producer(), "producer"); producerThread.start(); for (int i = 0; i < consumerThreads.length; i++) { consumerThreads[i].join(); } producerThread.join(); } }
เมธอด main() เริ่มต้นเธรดผู้บริโภคห้าเธรดและเธรดผู้ผลิตหนึ่งเธรด จากนั้นรอให้เธรดเหล่านั้นเสร็จสิ้น เธรดผู้ผลิตจะเพิ่มค่าใหม่ให้กับคิวและแจ้งเตือนเธรดที่รออยู่ทั้งหมดว่ามีบางอย่างเกิดขึ้น ผู้บริโภคจะได้รับการล็อกคิว (เช่น ผู้บริโภคแบบสุ่มหนึ่งราย) จากนั้นจึงเข้าสู่โหมดสลีป ซึ่งจะถูกเพิ่มในภายหลังเมื่อคิวเต็มอีกครั้ง เมื่อผู้ผลิตทำงานเสร็จก็จะแจ้งให้ผู้บริโภคทุกคนตื่น หากเราไม่ดำเนินการขั้นตอนสุดท้าย เธรดผู้บริโภคจะรอการแจ้งเตือนถัดไปตลอดไป เนื่องจากเราไม่ได้ตั้งค่าการหมดเวลาให้รอ แต่เราสามารถใช้วิธี wait (long timeout) เพื่อปลุกให้ตื่นอย่างน้อยหลังจากผ่านไประยะหนึ่งแล้ว
ตามที่ระบุไว้ในส่วนก่อนหน้า การเรียก wait() บนมอนิเตอร์ของอ็อบเจ็กต์จะเป็นการปลดล็อคบนมอนิเตอร์นั้นเท่านั้น ล็อคอื่นๆ ที่ยึดด้วยเธรดเดียวกันจะไม่ถูกคลายออก ตามที่เข้าใจง่าย ในการทำงานทุกวัน การเรียกเธรด wait() อาจช่วยล็อคต่อไปได้ หากเธรดอื่นกำลังรอการล็อกเหล่านี้อยู่ สถานการณ์การล็อกตายอาจเกิดขึ้นได้ ลองดูการล็อคในตัวอย่างต่อไปนี้:
public class SynchronizedAndWait { private static final Queue queue = new ConcurrentLinkedQueue(); public synchronized Integer getNextInt() { Integer retVal = null; while (retVal == null) { synchronized (queue) { try { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } retVal = queue.poll(); } } return retVal; } public synchronized void putInt(Integer value) { synchronized (queue) { queue.add(value); queue.notify(); } } public static void main(String[] args) throws InterruptedException { final SynchronizedAndWait queue = new SynchronizedAndWait(); Thread thread1 = new Thread(new Runnable() { public void run() { for (int i = 0; i < 10; i++) { queue.putInt(i); } } }); Thread thread2 = new Thread(new Runnable() { public void run() { for (int i = 0; i < 10; i++) { Integer nextInt = queue.getNextInt(); System.out.println("Next int: " + nextInt); } } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); } }
ตามที่เรา
ได้เรียนรู้ก่อนหน้านี้การเพิ่ม synchronized ให้กับลายเซ็นเมธอดจะเทียบเท่ากับการสร้างบล็อก synchronized(this){} ในตัวอย่างข้างต้น เราได้เพิ่มคีย์เวิร์ดที่ซิงโครไนซ์เข้ากับวิธีการโดยไม่ตั้งใจ จากนั้นซิงโครไนซ์คิวกับมอนิเตอร์ของออบเจ็กต์คิวเพื่อส่งเธรดนี้เข้าสู่โหมดสลีปในขณะที่รอค่าถัดไปจากคิว จากนั้น เธรดปัจจุบันจะปล่อยการล็อกบนคิว แต่ไม่ใช่การล็อกในคิวนี้ เมธอด putInt() แจ้งเตือนเธรดที่กำลังนอนหลับว่ามีการเพิ่มค่าใหม่ แต่บังเอิญเราได้เพิ่มคำหลักที่ซิงโครไนซ์เข้ากับวิธีนี้เช่นกัน ตอนนี้เธรดที่สองหลับไปแล้ว มันยังคงล็อคอยู่ ดังนั้น เธรดแรกไม่สามารถป้อนเมธอด putInt() ได้ในขณะที่เธรดที่สองล็อคไว้ เป็นผลให้เรามีสถานการณ์การหยุดชะงักและโปรแกรมค้าง หากคุณรันโค้ดข้างต้น มันจะเกิดขึ้นทันทีหลังจากที่โปรแกรมเริ่มทำงาน ในชีวิตประจำวันสถานการณ์นี้อาจไม่ชัดเจนนัก การล็อคที่ถือโดยเธรดอาจขึ้นอยู่กับพารามิเตอร์และเงื่อนไขที่พบในรันไทม์ และบล็อกที่ซิงโครไนซ์ซึ่งทำให้เกิดปัญหาอาจไม่ใกล้เคียงกับตำแหน่งที่เราวางการเรียก wait() ทำให้ยากต่อการค้นหาปัญหาดังกล่าว โดยเฉพาะอย่างยิ่งเนื่องจากอาจเกิดขึ้นเมื่อเวลาผ่านไปหรือมีภาระงานสูง
บ่อยครั้งที่คุณต้องตรวจสอบว่าตรงตามเงื่อนไขบางประการก่อนดำเนินการใดๆ กับออบเจ็กต์ที่ซิงโครไนซ์ เมื่อคุณมีคิว คุณต้องการที่จะรอให้คิวเต็ม ดังนั้นคุณสามารถเขียนวิธีการตรวจสอบว่าคิวเต็มหรือไม่ หากยังว่างอยู่ ให้ส่งเธรดปัจจุบันเข้าสู่โหมดสลีปจนกว่าจะถูกปลุกขึ้นมา:
public Integer getNextInt() { Integer retVal = null; synchronized (queue) { try { while (queue.isEmpty()) { queue.wait(); } } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (queue) { retVal = queue.poll(); if (retVal == null) { System.err.println("retVal is null"); throw new IllegalStateException(); } } return retVal; }
โค้ดด้านบนจะซิงโครไนซ์กับคิวก่อนที่จะเรียก wait() จากนั้นรอใน while จนกระทั่งองค์ประกอบอย่างน้อยหนึ่งรายการปรากฏในคิว บล็อกซิงโครไนซ์ที่สองใช้คิวเป็นตัวตรวจสอบอ็อบเจ็กต์อีกครั้ง โดยจะเรียกเมธอด poll() ของคิวเพื่อรับค่า เพื่อวัตถุประสงค์ในการสาธิต IllegalStateException จะถูกส่งออกมาเมื่อการสำรวจความคิดเห็นส่งกลับค่าว่าง สิ่งนี้เกิดขึ้นเมื่อคิวไม่มีองค์ประกอบที่จะดึงข้อมูล เมื่อคุณเรียกใช้ตัวอย่างนี้ คุณจะเห็นว่า IllegalStateException ถูกส่งบ่อยมาก แม้ว่าเราจะซิงโครไนซ์อย่างถูกต้องโดยใช้ตัวตรวจสอบคิว แต่ก็มีข้อยกเว้นเกิดขึ้น เหตุผลก็คือเรามีบล็อกที่ซิงโครไนซ์สองบล็อกที่แตกต่างกัน ลองนึกภาพเรามีสองเธรดที่มาถึงบล็อกซิงโครไนซ์แรก เธรดแรกเข้าสู่บล็อกและเข้าสู่โหมดสลีปเนื่องจากคิวว่างเปล่า เช่นเดียวกับเธรดที่สอง ตอนนี้ทั้งสองเธรดตื่นแล้ว (ต้องขอบคุณการเรียก notifyAll() ที่ถูกเรียกโดยเธรดอื่นสำหรับมอนิเตอร์) ทั้งสองเธรดเห็นค่า (รายการ) ในคิวที่เพิ่มโดยผู้ผลิต จากนั้นทั้งสองก็มาถึงปราการที่สอง ที่นี่เธรดแรกที่ป้อนและดึงค่าจากคิว เมื่อเธรดที่สองเข้ามา คิวก็ว่างเปล่าแล้ว ดังนั้นจึงได้รับค่าว่างเป็นค่าที่ส่งคืนจากคิวและส่งข้อยกเว้น เพื่อป้องกันสถานการณ์ดังกล่าว คุณต้องดำเนินการทั้งหมดที่ขึ้นอยู่กับสถานะของมอนิเตอร์ในบล็อกซิงโครไนซ์เดียวกัน:
public Integer getNextInt() { Integer retVal = null; synchronized (queue) { try { while (queue.isEmpty()) { queue.wait(); } } catch (InterruptedException e) { e.printStackTrace(); } retVal = queue.poll(); } return retVal; }
ที่นี่เราดำเนินการวิธี poll() ในบล็อกที่ซิงโครไนซ์เดียวกันกับวิธี isEmpty() ขอบคุณบล็อกที่ซิงโครไนซ์ เรามั่นใจว่ามีเพียงเธรดเดียวเท่านั้นที่กำลังดำเนินการวิธีการสำหรับจอภาพนี้ในเวลาที่กำหนด ดังนั้นจึงไม่มีเธรดอื่นใดที่สามารถลบองค์ประกอบออกจากคิวระหว่างการเรียก isEmpty() และ poll() แปลต่อได้
ที่นี่ครับ
GO TO FULL VERSION