JavaRush /จาวาบล็อก /Random-TH /พื้นฐานของการทำงานพร้อมกัน: การหยุดชะงักและการตรวจสอบวัตถ...
Snusmum
ระดับ
Хабаровск

พื้นฐานของการทำงานพร้อมกัน: การหยุดชะงักและการตรวจสอบวัตถุ (ส่วนที่ 1, 2) (การแปลบทความ)

เผยแพร่ในกลุ่ม
บทความที่มา: 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 ที่เก็บเธรดในเครื่อง
1. ความมีชีวิตชีวา
เมื่อพัฒนาแอปพลิเคชันที่ใช้การทำงานแบบขนานเพื่อให้บรรลุเป้าหมาย คุณอาจพบสถานการณ์ที่เธรดที่แตกต่างกันสามารถบล็อกซึ่งกันและกันได้ หากแอปพลิเคชันทำงานช้ากว่าที่คาดไว้ในสถานการณ์นี้ เราจะบอกว่าแอปพลิเคชันไม่ทำงานตามที่คาดไว้ ในส่วนนี้ เราจะมาดูปัญหาที่อาจคุกคามความอยู่รอดของแอปพลิเคชันแบบมัลติเธรดโดยละเอียดยิ่งขึ้น
1.1 การปิดกั้นซึ่งกันและกัน
คำว่า 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 ให้ความสามารถในการกำหนดวันหมดอายุสำหรับการล็อค - ดังที่เราเห็นจากตัวอย่างข้างต้น การชะงักงันจะไม่เกิดขึ้นหากลำดับของการร้องขอไม่แตกต่างกันในเธรดที่ต่างกัน วิธีนี้ควบคุมได้ง่ายหากคุณสามารถใส่โค้ดการบล็อกทั้งหมดไว้ในวิธีเดียวที่เธรดทั้งหมดต้องผ่าน ในแอปพลิเคชันขั้นสูง คุณอาจพิจารณาใช้ระบบตรวจจับการหยุดชะงักด้วยซ้ำ ที่นี่ คุณจะต้องใช้ลักษณะคล้ายการตรวจสอบเธรด ซึ่งแต่ละเธรดจะรายงานว่าได้รับการล็อคสำเร็จแล้ว และกำลังพยายามรับการล็อค หากเธรดและการล็อกถูกจำลองเป็นกราฟกำกับ คุณสามารถตรวจพบได้ว่าเมื่อเธรดที่ต่างกันสองเธรดเก็บทรัพยากรไว้ในขณะที่พยายามเข้าถึงทรัพยากรที่ถูกล็อกอื่น ๆ ในเวลาเดียวกัน หากคุณสามารถบังคับให้เธรดที่บล็อกปล่อยทรัพยากรที่จำเป็นได้ คุณสามารถแก้ไขสถานการณ์การชะงักงันได้โดยอัตโนมัติ
1.2 การถือศีลอด
ตัวกำหนดตารางเวลาตัดสินใจว่าควรดำเนินการ เธรดใด ในสถานะ RUNNABLE ต่อไป การตัดสินใจจะขึ้นอยู่กับลำดับความสำคัญของเธรด ดังนั้นเธรดที่มีลำดับความสำคัญต่ำกว่าจะได้รับเวลา CPU น้อยกว่าเมื่อเปรียบเทียบกับเธรดที่มีลำดับความสำคัญสูงกว่า สิ่งที่ดูเหมือนเป็นวิธีแก้ปัญหาที่สมเหตุสมผลอาจทำให้เกิดปัญหาได้หากถูกละเมิด หากเธรดที่มีลำดับความสำคัญสูงดำเนินการอยู่เกือบตลอดเวลา ดูเหมือนว่าเธรดที่มีลำดับความสำคัญต่ำจะขาดแคลนเนื่องจากไม่มีเวลาเพียงพอในการทำงานอย่างถูกต้อง ดังนั้นจึงขอแนะนำให้ตั้งค่าลำดับความสำคัญของเธรดเฉพาะเมื่อมีเหตุผลที่น่าสนใจเท่านั้น ตัวอย่างที่ไม่ชัดเจนของการอดอาหารของเธรดจะได้รับ เช่น โดยเมธอด Finalize() เป็นวิธีสำหรับภาษา Java ในการรันโค้ดก่อนที่อ็อบเจ็กต์จะถูกรวบรวมแบบขยะ แต่ถ้าคุณดูที่ลำดับความสำคัญของเธรดที่กำลังปิดท้าย คุณจะสังเกตเห็นว่าเธรดนั้นไม่ได้ทำงานด้วยลำดับความสำคัญสูงสุด ผลที่ตามมา การอดอาหารของเธรดเกิดขึ้นเมื่อเมธอด Finalize() ของอ็อบเจ็กต์ของคุณใช้เวลามากเกินไปเมื่อเทียบกับส่วนที่เหลือของโค้ด ปัญหาอีกประการหนึ่งเกี่ยวกับเวลาดำเนินการเกิดขึ้นจากข้อเท็จจริงที่ว่าไม่ได้กำหนดไว้ในลำดับที่เธรดจะข้ามบล็อกที่ซิงโครไนซ์ เมื่อเธรดแบบขนานจำนวนมากกำลังสำรวจโค้ดบางส่วนที่ถูกเฟรมในบล็อกที่ซิงโครไนซ์ อาจเกิดขึ้นได้ว่าบางเธรดต้องรอนานกว่าเธรดอื่นก่อนที่จะเข้าสู่บล็อก ตามทฤษฎีแล้ว พวกเขาอาจไม่เคยไปถึงที่นั่นเลย วิธีแก้ปัญหานี้เรียกว่าการบล็อกที่ "ยุติธรรม" การล็อคที่ยุติธรรมจะคำนึงถึงเวลารอของเธรดเมื่อพิจารณาว่าใครจะผ่านเป็นคนต่อไป ตัวอย่างการใช้งานการล็อคโดยชอบธรรมมีอยู่ใน Java SDK: java.util.concurrent.locks.ReentrantLock หากใช้คอนสตรัคเตอร์โดยตั้งค่าสถานะบูลีนเป็นจริง ReentrantLock จะให้สิทธิ์เข้าถึงเธรดที่รอนานที่สุด สิ่งนี้รับประกันว่าจะไม่มีความหิวโหย แต่ในขณะเดียวกันก็นำไปสู่ปัญหาการเพิกเฉยต่อลำดับความสำคัญ ด้วยเหตุนี้ กระบวนการที่มีลำดับความสำคัญต่ำกว่าซึ่งมักจะรออยู่ที่อุปสรรคนี้อาจทำงานบ่อยขึ้น สุดท้ายแต่ไม่ท้ายสุด คลาส ReentrantLock สามารถพิจารณาเฉพาะเธรดที่กำลังรอการล็อคเท่านั้น เช่น กระทู้ที่เปิดตัวบ่อยเพียงพอและถึงอุปสรรค หากลำดับความสำคัญของเธรดต่ำเกินไป สิ่งนี้จะไม่เกิดขึ้นบ่อยนัก ดังนั้นเธรดที่มีลำดับความสำคัญสูงจะยังคงผ่านการล็อกบ่อยกว่า
2. การตรวจสอบวัตถุพร้อมกับ wait() และ notify()
ในการประมวลผลแบบมัลติเธรด สถานการณ์ทั่วไปคือการมีเธรดผู้ปฏิบัติงานรอให้ผู้ผลิตสร้างงานบางอย่างให้พวกเขา แต่อย่างที่เราได้เรียนรู้ การรอคอยอย่างแข็งขันในขณะที่ตรวจสอบค่าที่แน่นอนไม่ใช่ตัวเลือกที่ดีในแง่ของเวลา 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) เพื่อปลุกให้ตื่นอย่างน้อยหลังจากผ่านไประยะหนึ่งแล้ว
2.1 บล็อกซิงโครไนซ์ที่ซ้อนกันพร้อม wait() และ notify()
ตามที่ระบุไว้ในส่วนก่อนหน้า การเรียก 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() ทำให้ยากต่อการค้นหาปัญหาดังกล่าว โดยเฉพาะอย่างยิ่งเนื่องจากอาจเกิดขึ้นเมื่อเวลาผ่านไปหรือมีภาระงานสูง
2.2 เงื่อนไขในบล็อกซิงโครไนซ์
บ่อยครั้งที่คุณต้องตรวจสอบว่าตรงตามเงื่อนไขบางประการก่อนดำเนินการใดๆ กับออบเจ็กต์ที่ซิงโครไนซ์ เมื่อคุณมีคิว คุณต้องการที่จะรอให้คิวเต็ม ดังนั้นคุณสามารถเขียนวิธีการตรวจสอบว่าคิวเต็มหรือไม่ หากยังว่างอยู่ ให้ส่งเธรดปัจจุบันเข้าสู่โหมดสลีปจนกว่าจะถูกปลุกขึ้นมา: 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() แปลต่อได้ ที่นี่ครับ
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION