wait وnotify والتنسيق بين الخيوط
wait وnotify والتنسيق بين الخيوط
يمنع الاستبعاد المتبادل الخيوطَ من إفساد الحالة المشتركة، لكنه لا يساعدها على التعاون. فكّر في ثنائي المنتج-المستهلك الكلاسيكي: لا يجوز للمستهلك المضي قدمًا حتى يتوفر بيانات يستهلكها، ولا يجوز للمنتج أن يفيض المخزن المؤقت المحدود. كلا الخيطين يحتاجان طريقة للإيقاف المؤقت والاستئناف بناءً على شرط معين — وهذا بالضبط ما توفره wait وnotify وnotifyAll.
نمط الكتلة المحروسة
الكتلة المحروسة هي الأسلوب الأساسي: يتحقق خيط من شرط، وإن لم يكن الشرط محققًا بعد، يوقف نفسه داخل المراقِب ريثما يُشير خيط آخر إلى أن شيئًا قد تغير. يُحرَّر الخيط النائم من القفل أثناء انتظاره، لتتمكن الخيوط الأخرى من دخول المراقِب والمضي قدمًا.
object.wait() فإنه بصورة ذرية (1) يُحرِّر مراقِب الكائن object، (2) يوقف نفسه، (3) يُعيد اكتساب المراقِب قبل العودة. هذا ما يجعل التنسيق ممكنًا: يتنحى الخيط المنتظِر جانبًا ليتمكن الخيط المُشعِر من دخول نفس الكتلة المزامَنة.
الهيكل الصحيح يستخدم دائمًا حلقة while، لا if:
if خاطئة: (1) الإيقاظ الوهمي — يُسمح لـ JVM بإيقاظ خيط منتظِر حتى دون أن يستدعي أحد notify؛ (2) الإشارات الضائعة — بين لحظة إيقاظ الخيط ولحظة إعادة اكتسابه للقفل، قد يكون خيط آخر قد استهلك العنصر الذي حقق الشرط. حلقة while تحمي من كلتيهما.
notify مقابل notifyAll
تُيقظ notify() خيطًا واحدًا بالضبط ينتظر على نفس المراقِب — يختاره مجدوَل JVM وغير قابل للتنبؤ. أما notifyAll() فتُيقظ كل الخيوط المنتظِرة؛ يتسابق كل منها لإعادة اكتساب القفل، ويعود جميعها ما عدا واحدًا إلى الانتظار.
- استخدم
notify()عندما تنتظر جميع الخيوط نفس الشرط ولا يستطيع المضي قدمًا إلا خيط واحد في كل مرة. مجموعة من العمال المتطابقين هي المثال المدرسي. - استخدم
notifyAll()عندما تنتظر خيوط مختلفة شروطًا مختلفة على نفس المراقِب (كمنتج ومستهلك ينتظران معًا على كائن واحد). قد تُيقظnotify()الخيط الخاطئ الذي يعود إلى النوم، تاركًا الخيط الجاهز معلقًا — وهو فشل في الحيوية.
notify() فقط بعد أن تتحقق أن كل خيط ينتظر على المراقِب ينتظر بالضبط نفس الشرط.
مثال كامل: المنتج والمستهلك
المثال أدناه يُنمذج مخزنًا مؤقتًا لعنصر واحد: ينتج المنتج عنصرًا ثم ينتظر حتى يأخذه المستهلك، والعكس بالعكس.
لاحظ أن كلًا من put وtake مزامَنتان على this، لذا يشتركان في نفس المراقِب. عندما يستدعي المنتج wait()، يُحرِّر القفل ليتمكن المستهلك من دخول take(). بعد استدعاء المستهلك notifyAll() وخروجه، يُعيد المنتج اكتساب القفل، يجد hasItem == false، ويتابع.
إطار اختبار بسيط:
ثلاث قواعد لا يجوز كسرها
- استدعِ wait/notify/notifyAll دائمًا داخل كتلة synchronized على نفس الكائن. استدعاؤها خارجها يرمي
IllegalMonitorStateExceptionفي وقت التشغيل. - أعِد دائمًا التحقق من الشرط في حلقة while بعد العودة من wait. الإيقاظات الوهمية والخيوط المتنافسة تجعل نسخة
ifغير آمنة. - استخدم notifyAll ما لم تستطع إثبات صحة notify. الإيقاظ الضائع مع
notify()يترك خيطًا معلقًا للأبد — وهو أصعب نوع من الأخطاء للإعادة الإنتاجية.
التعامل الصحيح مع InterruptedException
تُعلن wait() عن throws InterruptedException. إذا استدعى خيط آخر interrupt() على خيط منتظِر، ترمي wait() هذا الاستثناء فورًا. الاستجابة الصحيحة في الغالب هي استعادة حالة المقاطعة والسماح للاستثناء بالانتشار:
ابتلاع الاستثناء صامتًا (كتلة catch فارغة) يُتلف الإشارة ويجعل إيقاف الخيط بشكل نظيف أمرًا مستحيلًا.
wait/notify مقابل البدائل ذات المستوى الأعلى
قدّمت حزمة java.util.concurrent في Java 5 أدوات ذات مستوى أعلى مبنية فوق نفس الآليات الأساسية:
BlockingQueue(LinkedBlockingQueue،ArrayBlockingQueue) — قناة منتج-مستهلك جاهزة محدودة أو غير محدودة. فضّلها على بناء مخزن مؤقت يدوي بـ wait/notify.Condition(منReentrantLock.newCondition()) — يوفرawait()/signal()بنفس دلالات wait/notify لكنه يسمح بـ شروط متعددة مسماة على قفل واحد ويدعم الانتظار المحدد بوقت بشكل أنظف.CountDownLatch،CyclicBarrier،Semaphore— مزامِنات مخصصة لنقاط العد التنازلي أحادية الاستخدام، ونقاط التقاء الخيوط، وتحديد المعدل.
BlockingQueue أو Condition. يظل فهم wait/notify ضروريًا: إنه يدعم كل أداة ذات مستوى أعلى، ويظهر في قواعد الكود القديمة التي ستحتاج للصيانة، وتختبره أسئلة المقابلات بانتظام. وهو أيضًا الخيار الوحيد حين لا تستطيع إدخال تبعية على java.util.concurrent.
الخلاصة
الكتل المحروسة المبنية حول wait وnotifyAll هي أساس التنسيق بين الخيوط في Java. القواعد صارمة لكنها متسقة: امتلك دائمًا المراقِب، ضع دائمًا حلقة، فضّل دائمًا notifyAll، وأعِد دائمًا نشر InterruptedException. في الدرس التالي سنفحص حالات التوقف التام — ما هي، وكيف تشخّصها، وكيف تتجنبها من خلال التصميم.