أساسيّات التزامن

دورة حياة الخيط

15 دقيقة الدرس 3 من 13

دورة حياة الخيط

فهم ما يفعله الخيط في أي لحظة أمرٌ أساسي لكتابة كود متزامن صحيح. تُصنّف Java وجود الخيط على شكل آلة ذات حالات محدودة: ينتقل الخيط عبر مجموعة من الحالات المُعرَّفة تعريفًا واضحًا، من لحظة إنشائه حتى انتهائه. معرفة هذه الحالات — والأساليب التي تُحدث الانتقال بينها — تُمكّنك من استيعاب سلوك البرنامج، وتشخيص حالات التعليق، وكتابة منطق تنسيق يعمل فعليًا.

الحالات الست للخيط

يُعرّف تعداد Thread.State (المُقدَّم في Java 5 والمستمر حتى Java 17+) ستّ حالات:

  • NEW — تم إنشاء كائن Thread لكن لم يُستدعَ start() بعد. لا يوجد خيط نظام تشغيل حتى الآن.
  • RUNNABLE — بدأ تشغيل الخيط. إمّا أنه يعمل فعليًا على نواة معالج الآن، أو أنه جاهز للتشغيل وينتظر من جدولة نظام التشغيل أن تمنحه نواة. لا تُميّز Java في تعداد الحالة بين "يعمل" و"جاهز للعمل".
  • BLOCKED — الخيط ينتظر الحصول على قفل مراقب جوهري (كتلة synchronized أو أسلوب يحتجزه خيط آخر). لا يمكنه المتابعة حتى يُفرَج عن القفل.
  • WAITING — الخيط معلّق إلى أجل غير مسمى، ينتظر أن يتخذ خيطٌ آخر إجراءً محددًا. يحدث هذا عند استدعاء Object.wait()، أو Thread.join() بلا مهلة، أو LockSupport.park().
  • TIMED_WAITING — مثل WAITING لكن بمدة زمنية قصوى. الأساليب التي تسبّب هذه الحالة: Thread.sleep(millis)، وObject.wait(millis)، وThread.join(millis)، وLockSupport.parkNanos().
  • TERMINATED — عاد أسلوب run() الخاص بالخيط، أو أنهته استثناءٌ غير معالَج. كائن Thread لا يزال موجودًا في الذاكرة لكن لا يمكن إعادة تشغيله أبدًا.
BLOCKED مقابل WAITING: كلتاهما تعني أن الخيط معلّق، لكن السبب مختلف. BLOCKED دائمًا تتعلق بالحصول على قفل مراقب. أما WAITING وTIMED_WAITING فتتعلقان بإشارات التنسيق — حيث أفرج الخيط طوعًا عن القفل وينتظر إشعارًا أو انتهاء مهلة.

يمكنك فحص الحالة الراهنة لخيط ما في وقت التشغيل باستخدام thread.getState(). هذا مفيد بصفة رئيسية للتشخيص والأدوات، لا للتحكم في التدفق.

sleep — إيقاف الخيط الحالي مؤقتًا

يجعل Thread.sleep(long millis) الخيطَ الذي يعمل حاليًا يدخل TIMED_WAITING لمدة لا تقل عن عدد الميلي ثانية المحدد، ثم يعود إلى RUNNABLE. إنه أسلوب ساكن (static) — لا يمكنك جعل خيط آخر ينام.

public class SleepDemo { public static void main(String[] args) throws InterruptedException { System.out.println("Starting work..."); Thread.sleep(2_000); // النوم لمدة ثانيتين System.out.println("Resumed after sleep."); } }

حقيقتان مهمتان عن sleep:

  • لا يُفرج عن أقفال المراقب. إذا احتجز الخيط النائم قفل synchronized، فإن الخيوط الأخرى المنتظرة لذلك القفل تبقى محجوبة (BLOCKED) طوال مدة النوم. هذا مصدر شائع لضعف الأداء.
  • المدة هي حدٌّ أدنى، وليست ضمانًا. قد يستأنف جدول نظام التشغيل الخيطَ بعد وقت أطول قليلًا من المطلوب، خاصة على نظام مُثقَل.
public class SleepKeepsLock { private static final Object LOCK = new Object(); public static void main(String[] args) throws InterruptedException { Thread holder = new Thread(() -> { synchronized (LOCK) { System.out.println("Holder: acquired lock, sleeping..."); try { Thread.sleep(3_000); // القفل لا يُفرَج عنه أثناء النوم } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Holder: done."); } }); Thread waiter = new Thread(() -> { System.out.println("Waiter: trying to acquire lock (BLOCKED)..."); synchronized (LOCK) { System.out.println("Waiter: finally got the lock."); } }); holder.start(); Thread.sleep(100); // دع holder يأخذ القفل أولًا waiter.start(); } }

شغّل هذا وسترى أن المنتظر يبقى في حالة BLOCKED طوال 3 ثوانٍ كاملة.

join — انتظار انتهاء خيط آخر

يجعل thread.join() الخيطَ المُستدعي يدخل WAITING (أو TIMED_WAITING مع معامل المهلة) حتى يصل thread إلى TERMINATED. إنه الطريقة القياسية للانتظار حتى ينتهي عمل خلفي قبل استخدام نتائجه.

public class JoinDemo { public static void main(String[] args) throws InterruptedException { Thread worker = new Thread(() -> { System.out.println("Worker: computing..."); try { Thread.sleep(1_500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Worker: done."); }); worker.start(); System.out.println("Main: waiting for worker..."); worker.join(); // الخيط الرئيسي يدخل WAITING هنا System.out.println("Main: worker finished, continuing."); } }

النسخة ذات المهلة worker.join(millis) تعود بعد انتهاء المهلة حتى لو كان العامل لا يزال يعمل — تحقق دائمًا من worker.isAlive() بعدها إذا كانت النتيجة مهمة.

الانضمام لقائمة من الخيوط: كرّر على القائمة واستدعِ join() على كل منها. مجموع الوقت الفعلي يقارب مدة أطول مهمة، لا مجموع المهام — إذ تعمل المهام كلها بالتوازي، وتنتظر الحلقة ببساطة كل واحدة بالتسلسل.

interrupt — طلب الإلغاء

نموذج الإلغاء في Java تعاوني. لا تُوقف الخيط قسرًا؛ بل تُعيّن علامة الإقطاع الخاصة به باستدعاء thread.interrupt()، ويُتوقع من الخيط أن يلاحظ ذلك ويتوقف طوعًا.

هناك طريقتان يلاحظ بهما الخيط علامة الإقطاع:

  • الأساليب الحاجبة تُطلق InterruptedException. عندما يكون خيط في حالة WAITING أو TIMED_WAITING (نائم، أو منضم، أو منتظر على مراقب عبر Object.wait()) ويستدعي خيطٌ آخر interrupt() عليه، يُطلق الاستدعاء الحاجب InterruptedException وتُمسَح علامة الإقطاع.
  • الاستطلاع باستخدام Thread.interrupted() أو thread.isInterrupted(). يمكن للحلقات الطويلة المرتبطة بالمعالج أن تتحقق من العلامة دوريًا.
public class InterruptDemo { public static void main(String[] args) throws InterruptedException { Thread worker = new Thread(() -> { int count = 0; while (!Thread.currentThread().isInterrupted()) { count++; if (count % 1_000_000 == 0) { System.out.println("Working... " + count); } } System.out.println("Worker: interrupt flag set, stopping cleanly."); }); worker.start(); Thread.sleep(50); // دعه يعمل قليلًا worker.interrupt(); // طلب الإلغاء worker.join(); System.out.println("Main: worker has stopped."); } }
لا تبتلع InterruptedException صامتًا أبدًا. النمط catch (InterruptedException e) { /* تجاهل */ } يمسح علامة الإقطاع دون التصرف بناءً عليها، مما يجعل الخيط غير استجابي للإلغاء. إمّا أعِد إطلاق الاستثناء (إذا أتاح توقيع الأسلوب ذلك)، أو أعِد تعيين العلامة باستخدام Thread.currentThread().interrupt() قبل الإرجاع.

النمط الصحيح لإعادة التعيين:

try { Thread.sleep(1_000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // أعِد تعيين العلامة return; // ثم توقف بشكل نظيف }

تجميع كل شيء: تفصيل انتقالات الحالة

تأمّل هذا التسلسل لخيط واحد:

  1. new Thread(task) — الحالة NEW.
  2. thread.start() — تُنشئ JVM خيط نظام تشغيل؛ تصبح الحالة RUNNABLE.
  3. داخل task، يُستدعى Thread.sleep(500) — تصبح الحالة TIMED_WAITING.
  4. تنتهي مدة النوم — تعود الحالة إلى RUNNABLE.
  5. تحاول المهمة الدخول في كتلة synchronized يحتجزها خيط آخر — تصبح الحالة BLOCKED.
  6. يُفرَج عن القفل — تعود الحالة إلى RUNNABLE.
  7. يُرجع run() — تصبح الحالة TERMINATED.
public class LifecycleInspector { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); System.out.println("After new: " + t.getState()); // NEW t.start(); System.out.println("After start: " + t.getState()); // RUNNABLE Thread.sleep(100); System.out.println("While sleep: " + t.getState()); // TIMED_WAITING t.join(); System.out.println("After join: " + t.getState()); // TERMINATED } }

الخلاصة

ينتقل خيط Java عبر ست حالات مُعرَّفة بوضوح: NEW → RUNNABLE → (BLOCKED / WAITING / TIMED_WAITING) → TERMINATED. الأساليب الرئيسية التي تحكم هذه الانتقالات هي Thread.sleep() (إيقاف طوعي دون الإفراج عن الأقفال)، وThread.join() (انتظار انتهاء خيط آخر)، وThread.interrupt() (إلغاء تعاوني عبر علامة). إتقان هذه الانتقالات هو أساس كل بنية تزامن ذات مستوى أعلى مُغطاة في بقية هذه الدورة التعليمية.