مقدمة: لماذا نحتاج قوائم المهام في Java؟
\n\nفي التطوير الحديث باستخدام Java، تعدد الخيوط ليس رفاهية بل ضرورة. التطبيقات تعالج آلاف الطلبات، وتنفذ مهام الخلفية، وتتفاعل مع الخدمات الخارجية. بدون تنظيم صحيح لقوائم المهام، يتحول الكود إلى فوضى: تمنع الخيوط بعضها البعض، وتمتلئ الذاكرة، وينخفض الأداء.
\n\nقوائم المهام (Task Queues) هي آلية تسمح بتوزيع العمل بكفاءة بين الخيوط. في Java، توجد مجموعة أدوات قوية في الحزمة java.util.concurrent. في هذه المقالة، سنشرح كيفية استخدام قوائم المهام عمليًا، مع أمثلة كود حقيقية.
ستتعلم:
\n- \n
- كيف تعمل
BlockingQueueوتطبيقاتها \n - كيفية إعداد
ThreadPoolExecutorلمهام محددة \n - كيفية تجنب الأخطاء الشائعة عند العمل مع القوائم \n
- كيفية اختبار وتصحيح كود تعدد الخيوط \n
أساسيات قوائم المهام: BlockingQueue
\n\nBlockingQueue هي واجهة تشكل أساس معظم قوائم المهام في Java. تدعم عمليات الانتظار: إذا كانت القائمة فارغة، ينتظر الخيط المستهلك حتى يظهر عنصر؛ إذا كانت القائمة ممتلئة، ينتظر الخيط المنتج حتى يتوفر مكان.
التطبيقات الرئيسية لـ BlockingQueue
\n\nفي Java، توجد عدة تطبيقات لـ BlockingQueue، لكل منها خصائصه:
- \n
- ArrayBlockingQueue — حجم ثابت، مبني على مصفوفة. مناسب للسيناريوهات ذات الحد المعروف للمهام. \n
- LinkedBlockingQueue — محدودة اختياريًا، مبنية على قائمة مرتبطة. جيدة للأحمال الديناميكية. \n
- PriorityBlockingQueue — قائمة غير محدودة ذات أولويات. تُستخدم عندما تكون للمهام أهمية مختلفة. \n
- SynchronousQueue — قائمة بدون سعة: كل insert يجب أن ينتظر remove المقابل. مثالية للنقل المباشر للمهام. \n
مثال: قائمة مهام بسيطة مع ArrayBlockingQueue
\n\nلننظر إلى مثال أساسي حيث يقوم خيط بتوليد المهام وآخر بمعالجتها:
\n\nimport java.util.concurrent.ArrayBlockingQueue;\nimport java.util.concurrent.BlockingQueue;\n\npublic class SimpleTaskQueue {\n public static void main(String[] args) {\n BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);\n\n // خيط منتج\n Thread producer = new Thread(() -> {\n try {\n for (int i = 1; i <= 20; i++) {\n String task = "المهمة رقم " + i;\n queue.put(task); // يُحظر إذا كانت القائمة ممتلئة\n System.out.println("تمت الإضافة: " + task);\n Thread.sleep(100); // محاكاة التوليد\n }\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n }\n });\n\n // خيط مستهلك\n Thread consumer = new Thread(() -> {\n try {\n while (true) {\n String task = queue.take(); // يُحظر إذا كانت القائمة فارغة\n System.out.println("تمت المعالجة: " + task);\n Thread.sleep(300); // محاكاة المعالجة\n }\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n }\n });\n\n producer.start();\n consumer.start();\n }\n}\n\n\nيوضح هذا الكود النمط الأساسي: يضيف المنتج المهام عبر put()، ويأخذها المستهلك عبر take(). كلا الطريقتين تُحظران عند الحاجة، مما يضمن التزامن الطبيعي.
ThreadPoolExecutor: قائمة مهام احترافية
\n\nفي الممارسة العملية، نادرًا ما يُستخد