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

إنشاء الخيوط

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

إنشاء الخيوط

في Java ثمة طريقتان كلاسيكيتان لتعريف وحدة العمل التي يُنفّذها الخيط: توسيع Thread أو تنفيذ Runnable. خيار ثالث — Callable مع Future — يُغطَّى في درس لاحق عن مجمّعات الخيوط. الآن نركّز على الأساسيات، والقاعدة الأهم التي يخطئ فيها المبتدئون يوميًا: استدعاء start() لا run().

الخيار الأول: توسيع Thread

لأن java.lang.Thread ينفّذ Runnable بذاته، يمكنك اشتقاق فئة منه وتجاوز run() مباشرةً:

public class CounterThread extends Thread { private final String label; public CounterThread(String label) { super(label); // يضبط اسم الخيط — مفيد في السجلّات this.label = label; } @Override public void run() { for (int i = 1; i <= 5; i++) { System.out.println(label + " → " + i + " [" + Thread.currentThread().getName() + "]"); } } } // تشغيله CounterThread t1 = new CounterThread("Thread-A"); CounterThread t2 = new CounterThread("Thread-B"); t1.start(); // ← يُنشئ خيط نظام تشغيل جديد ويستدعي run() عليه t2.start();

يعمل الخيطان بالتزامن: ستتداخل أسطر مخرجات Thread-A وThread-B بترتيب غير حتمي.

لا تستدعِ run() مباشرةً أبدًا. استدعاء t1.run() بدلًا من t1.start() لا يُنشئ خيطًا جديدًا — إنه استدعاء دالة عادي على الخيط الحالي، تمامًا كأي دالة أخرى. لا تزامن يحدث على الإطلاق. هذا من أكثر الأخطاء شيوعًا في كود التزامن للمبتدئين.

الخيار الثاني: تنفيذ Runnable

Runnable واجهة وظيفية (دالة مجردة واحدة: void run()). تُمرّر نسخةً منها إلى مُنشئ Thread:

public class CounterTask implements Runnable { private final String label; public CounterTask(String label) { this.label = label; } @Override public void run() { for (int i = 1; i <= 5; i++) { System.out.println(label + " → " + i + " [" + Thread.currentThread().getName() + "]"); } } } // تغليف Runnable في Thread وتشغيله Thread t1 = new Thread(new CounterTask("Task-A"), "worker-1"); Thread t2 = new Thread(new CounterTask("Task-B"), "worker-2"); t1.start(); t2.start();

لأن Runnable واجهة وظيفية، يمكنك كتابة المهمة مباشرةً بـ lambda — دون فئة منفصلة:

Runnable counter = () -> { for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + " → " + i); } }; new Thread(counter, "lambda-thread-1").start(); new Thread(counter, "lambda-thread-2").start();
Lambda تجعل المهام القصيرة مقروءة، لكن استخرج الـ lambda إلى فئة أو متغير مسمّى حين تتجاوز المنطق بضعة أسطر. فالـ lambda المجهولة يصعب تتبّعها في تتبّعات المكدس ويصعب اختبارها وحدها.

start() مقابل run() — الآلية الداخلية

فهم ما يفعله start() فعلًا يحول دون فئة كاملة من الأخطاء:

  • start() يطلب من JVM تخصيص خيط نظام تشغيل أصلي وجدولته ثم استدعاء run() على ذلك الخيط الجديد. يعود الخيط المُستدعي فورًا من start() ويكمل عمله الخاص.
  • run() مجرد دالة Java عادية. استدعاؤها مباشرةً ينفّذ المنطق على خيط المُستدعي بالتسلسل، دون أي تزامن.
  • استدعاء start() على خيط بدأ مسبقًا يُلقي IllegalThreadStateException. كائن Thread للاستخدام مرة واحدة فقط.
Thread t = new Thread(() -> System.out.println("Hello from " + Thread.currentThread().getName())); t.run(); // يطبع: Hello from main — لا خيط جديد t.start(); // يطبع: Hello from Thread-0 — خيط جديد // t.start(); // ← سيُلقي IllegalThreadStateException — لا يمكن إعادة الاستخدام

توسيع Thread مقابل تنفيذ Runnable — المقايضات

كلا الأسلوبين يُنشئان خيوطًا، لكنهما يختلفان في التصميم:

  • توسيع Thread — سهل في الأمثلة الصغيرة، لكن Java تدعم الوراثة المفردة فقط. إن كانت فئتك تمتدّ من شيء آخر فهذا الباب مغلق. كما أنه يربط منطق المهمة بآلية الخيط بإحكام.
  • تنفيذ Runnable — يفصل ماذا نفعل عن كيف يُجدوَل. يمكن تقديم نفس Runnable لمجمّع خيوط (ExecutorService)، أو تشغيله مباشرةً في الاختبارات، أو تغليفه في خيط افتراضي (Java 21). هذه المرونة هي السبب في أن Runnable هو الاختيار الصحيح دائمًا تقريبًا في كود الإنتاج.
قاعدة عملية: استخدم Runnable (أو lambda) للمهام؛ واستخدم فئة Thread فقط حين تحتاج تحديدًا لضبط خصائص الخيط (setDaemon، setName، setPriority) أو حين تمدّها لبناء نوع خيط متخصّص كعامل في مجمّع. في كود التطبيقات الحديث لن تمدّ Thread بالكاد أبدًا.

تسمية الخيوط

أعطِ الخيوط دائمًا أسماءً ذات معنى. الأسماء الافتراضية (Thread-0، Thread-1، …) عديمة الفائدة عند قراءة تفريغ خيوط أثناء حادثة في الإنتاج:

Thread worker = new Thread(this::processQueue, "order-processor-1"); worker.setDaemon(true); // خيوط الخدمة لا تمنع إغلاق JVM worker.start();

مع خيط مسمّى، يخبرك تتبّع المكدس فورًا بـأي مكوّن منطقي عالق أو يستنزف المعالج.

الخلاصة

ثمة طريقتان كلاسيكيتان لإنشاء الخيوط: توسيع Thread (تجاوز run()) أو تنفيذ Runnable (تمريره لمنشئ Thread). استدعِ start() دائمًا للحصول على خيط جديد — run() مجرد استدعاء دالة. فضّل Runnable أو lambda في كود الإنتاج لأنها تفصل المهمة عن آلية التنفيذ، وتبقي تسلسل الوراثة مفتوحًا، وتعمل بسلاسة مع ExecutorService والخيوط الافتراضية. في الدرس التالي سنرى الدورة الحياتية الكاملة للخيط من الإنشاء إلى الإنهاء.