أفضل الممارسات العمليّة لتعدّد الخيوط: طبعة Java ── اصطلاحات عصر الخيوط الافتراضيّة
· غو كومورا · تعدّد الخيوط, Java, تطبيقات الأعمال, التحقيق في الأخطاء, التصميم
«نريد موازاة دفعة أعمال في Java.» «ذاكرة التخزين المؤقّت المشتركة في تطبيق Spring للويب تفسد أحياناً.» «ورثنا تطبيقاً Swing قديماً مليئاً بـ new Thread.» — أدمجت Java تعدّد الخيوط في اللغة منذ JDK 1.0، ونضجت إلى صندوق أدوات java.util.concurrent، ثمّ أعادت مع الخيوط الافتراضيّة في JDK 21 كتابة الحكمة التقليديّة للبرمجة المتزامنة مرّة أخرى. ولأنّ مجموعة الأدوات غنيّة إلى هذا الحدّ، فإنّ الأداة التي تختارها تصبح جودة التصميم ذاتها.
هذا المقال هو طبعة Java من سلسلة تعدّد الخيوط في الممارسة. موجَّه إلى المطوّرين الذين يكتبون أنظمة أعمال ودفعات وتطبيقات خادم في Java، ويُسقِط مبادئ تصميم تعدّد الخيوط — لا تُنشئ الخيوط مباشرة، قلِّل الحالة المشتركة القابلة للتغيير، انضبط في الأقفال، صمِّم كيف توقف الأشياء قبل أيّ شيء آخر — على أدوات Java (مستهدفاً أساساً إصدار LTS، JDK 21 فما بعد)، ويجمع اختيارات عصر الخيوط الافتراضيّة والمطبّات الخاصّة بـ Java، مستنداً إلى مصادر أوّليّة حتّى أغسطس 2026. كُتب ليُقرأ وحده. المبادئ نفسها مطبَّقة على لغات أخرى تغطّيها المقالات المرافقة «طبعة .NET» و«طبعة C++» و«طبعة C».
1. الخلاصة أوّلاً
- لا تكتب
new Threadفي شيفرة الأعمال — القاعدة نفسها تنطبق في Java. سلِّم المهام إلىExecutorServiceودع المكتبة تدير دورات حياة الخيوط.1 - وجِّه المهام التي تنتظر I/O في الغالب إلى الخيوط الافتراضيّة. تُستخدم الخيوط الافتراضيّة، التي صارت رسميّة في JDK 21، خيطاً واحداً لكلّ مهمّة ويجب ألّا تُجمَّع في بركة أبداً. قيِّد التزامن بـ
Semaphoreلا بحجم البركة.23 - الخيوط الافتراضيّة أداة معدّل إنتاج، لا أداة لتسريع الحساب. موازاة الأعمال المرتبطة بالمعالج ما زالت من شأن خيوط المنصّة بحجم يقارب عدد الأنوية — بركة ثابتة أو parallel stream، كما من قبل.3
- اقفل على كائن قفل
private final، أو علىReentrantLockمخصَّص.synchronized(this)والقفل على كائن مكشوف علناً يمكن أن يصطدما بشيفرة خارجيّة. إن احتجت اكتساباً مؤقّتاً (tryLock) فاستخدمReentrantLock. - في JDK 21-23 كانت المشكلة أنّ الحظر داخل كتلة
synchronizedيثبّت الخيوط الافتراضيّة؛ أصلحه JDK 24 (JEP 491). ميِّز التحذيرات القديمة عن الواقع الحالي.34 volatileيضمن الرؤية والترتيب، لا الذرّيّة. استخدمAtomicInteger/LongAdderللعدّادات، والأقفال للحالة المركّبة.5- الإيقاف التعاوني عبر المقاطعة هو السبيل الصحيح الوحيد لإيقاف خيط.
Thread.stop/suspend/resumeترمي الآنUnsupportedOperationException. لا تبتلعInterruptedException— إمّا تستعيد الحالة أو تعيد الرمي.6 - أوقف
ExecutorServiceبالنمط ذي المرحلتين: shutdown → awaitTermination → shutdownNow.shutdownNowأفضل جهد (التنفيذ القياسي يعمل عبر المقاطعة)، لذا يفترض أنّ مهامك تستجيب للمقاطعة.1 - واجهة Swing تخصّ EDT (Event Dispatch Thread) حصراً. اطلب التحديثات من خيوط أخرى عبر
SwingUtilities.invokeLater.7
2. لماذا تعدّد الخيوط صعب ── حالات السباق والجمود ونموذج الذاكرة
اختصر المشكلات التي يدخلها تعدّد الخيوط، بغضّ النظر عن اللغة، تجد نوعين.
حالة السباق خطأ يعتمد فيه الناتج على ترتيب وصول خيوط متعدّدة إلى قطعة شيفرة معيّنة. المثال الكلاسيكي عدّاد مشترك: التعبير الواحد count++ ينقسم فعليّاً إلى ثلاث خطوات — اقرأ، أضِف، اكتب من جديد. إن دخل خيطان هذه الخطوات الثلاث في الوقت نفسه، تُمحى زيادة أحد الخيطين حين يكتب الآخر. يتغيّر الناتج في كلّ تشغيل، ولا سبيل للتنبّؤ بأيّ ناتج ستحصل.
sequenceDiagram
participant A as الخيط A
participant M as المتغيّرة المشتركة count
participant B as الخيط B
Note over M: count = 10
A->>M: قراءة (10)
B->>M: قراءة (10)
A->>A: إضافة محلّيّة (11)
B->>B: إضافة محلّيّة (11)
A->>M: كتابة (11)
B->>M: كتابة (11)
Note over M: حدثت زيادتان,<br/>لكن count = 11 — ضاعت زيادة الخيط A
الشكل 1: حالة سباق كلاسيكيّة تضيع فيها زيادة على عدّاد مشترك. إن تداخل خيط آخر أثناء الخطوات الثلاث لـ count++، فإنّ آخر كتابة تُكتب تطمس الأخرى
الجمود حالة ينتظر فيها خيطان كلٌّ منهما قفلاً يمسكه الآخر، فلا يتقدّم أيٌّ منهما. الخيط A يمسك القفل 1 وينتظر القفل 2؛ الخيط B يمسك القفل 2 وينتظر القفل 1 — ذلك وحده يكفي ليتوقّف كلاهما إلى الأبد.
flowchart LR
A["الخيط A<br/>يمسك القفل 1"] -->|"ينتظر تحرير القفل 2"| B["الخيط B<br/>يمسك القفل 2"]
B -->|"ينتظر تحرير القفل 1"| A
الشكل 2: الانتظار الدائري للجمود. لحظة تشكّل أسهم الانتظار حلقة، يتوقّف كلّ خيط داخل تلك الحلقة إلى الأبد
كلاهما يعتمد على التوقيت. تداخل يظهر مرّة كلّ عشرات الآلاف من التشغيلات على آلة تطوير يمكن أن يحدث كلّ يوم على خادم إنتاج بعدد أنوية وحمل مختلفين. «يتوقّف عن التكرار حين أُرفِق منقِّحاً» و«اختفى حين أضفت تسجيلاً» كلاهما لأنّ الرصد يغيّر التوقيت — سلوك كلاسيكي لخطأ سباق. لهذا بالضبط تشير كلّ مبادئ هذا المقال في اتّجاه تقليل الأماكن التي تحتاج مزامنة، قبل القلق بشأن المزامنة الصحيحة.
2.1. مقدّمة خاصّة بـ Java ── نموذج الذاكرة وhappens-before
فوق ذلك، ما يخصّ Java هو أنّ كيفيّة رؤية البيانات المشتركة تُعرَّف بـ علاقات happens-before في نموذج ذاكرة Java (JMM).
قراءة متغيّرة مشتركة وكتابتها بلا مزامنة لا تصبح «سلوكاً غير معرَّف» بأسلوب C++، لكن يمكن أن ينتج عنها شرعاً استمرار رؤية قيم قديمة، أو ظهور الكتابات بغير ترتيبها. خطأ اتّساق ذاكرة حيث «حلقة تراقب راية boolean، لكنّ القيمة التي غيّرها خيط آخر لا تظهر أبداً» سلوك يسمح به JMM، لا خطأ في JVM.5 الأدوات التي تحمي من هذا هي الآليّات التي تُنشئ علاقات happens-before — synchronized وvolatile وأصناف java.util.concurrent. استخدم المجموعات المتزامنة استخداماً صحيحاً وتضمن لك المكتبة «علاقة happens-before بين عمليّة تحديث واسترجاع لاحق».8
بعبارة أخرى، يمكن تلخيص التوجيه العملي لـ Java هكذا: لا تتذاكَ بمتغيّرة مشتركة خام. استخدم أدوات java.util.concurrent للمشاركة، ودع المكتبة تُنشئ علاقات happens-before.
3. كيف تُنشئ الخيوط ── ExecutorService والخيوط الافتراضيّة
3.1. فصل المهمّة عن كيفيّة تشغيلها
ExecutorService هو ما يحمل مبدأ «لا تُنشئ الخيوط بنفسك» في Java. يفصل العمل (Runnable / Callable) عن كيفيّة تنفيذه — كم خيطاً، وأيّ طابور — ويترك إنشاء الخيوط وإعادة استخدامها والتخلّص منها للمكتبة.1
من JDK 21 فصاعداً، صار اختيار كيفيّة التشغيل خياراً ثنائيّاً بسيطاً.23
flowchart TB
S["مهمّة تريد تشغيلها تزامنيّاً"] --> Q1{"ما الذي يقود المهمّة؟"}
Q1 -->|"انتظار I/O في الغالب<br/>استدعاءات HTTP وقاعدة بيانات وملفّات"| VT["خيوط افتراضيّة<br/>Executors.newVirtualThreadPerTaskExecutor<br/>واحدة لكلّ مهمّة، لا تُجمَّع أبداً"]
Q1 -->|"حساب مرتبط بالمعالج"| PT["بركة ثابتة من خيوط المنصّة<br/>Executors.newFixedThreadPool - تقريباً بعدد الأنوية<br/>أو parallel stream"]
VT --> LIMIT["قيِّد التزامن نحو الخدمات الخارجيّة<br/>بـ Semaphore، لا بحجم البركة"]
الشكل 3: اختيار كيفيّة تنفيذ العمل من JDK 21 فصاعداً. ارسم الخطّ أوّلاً — «غيِّر كيف تنتظر I/O، ووازِ للحساب» — ثمّ سلِّم أعمال I/O-bound إلى الخيوط الافتراضيّة وأعمال المعالج إلى بركة تقليديّة
هناك تحفّظ واحد في جانب الأعمال المرتبطة بالمعالج. يحدّ Executors.newFixedThreadPool عدد الخيوط، لكن طابوره بلا حدود. في خدمة طويلة العمر تتجاوز فيها الإرسالات المعالجة باستمرار، يُحدّ فقط عدد الخيوط بعدد الأنوية — أمّا المهام المتراكمة في الطابور وبياناتها فتظلّ تأكل الذاكرة. في ذلك النوع من الإعداد، إمّا استخدم ThreadPoolExecutor مباشرة لتهيئة طابور محدود مع سياسة رفض، أو ضع تحكّماً في القبول مثل Semaphore في جهة الإرسال حتّى تتمكّن من تطبيق ضغط عكسي (المبدأ نفسه في نقاش الطوابير في القسم 4).
3.2. لا تُسئ استخدام الخيوط الافتراضيّة
الخيوط الافتراضيّة خيوط خفيفة منفصلة عن خيوط نظام التشغيل: أثناء عمليّة حظر في JDK (I/O والقفل والنوم وما شابه في المكتبة القياسيّة) تُحرّر خيط نظام التشغيل، ولهذا يمكن لـ JVM واحدة أن تشغّل ملايين منها. لكنّها لا تحرّره لكلّ نوع من الحظر. إن حُظر خيط افتراضي أثناء تنفيذ شيفرة أصليّة (JNI) أو دالّة أجنبيّة، يبقى مثبتاً على خيطه الحامل. ما أصلحه JDK 24 (JEP 491، أدناه) كان التثبيت الذي يسبّبه synchronized؛ أمّا التثبيت عند الحدود الأصليّة فيبقى، لذا فإنّ تحميل عدد كبير من الخيوط الافتراضيّة بعمليّات تحظر طويلاً عبر مولِّد JNI أو واجهة جهاز سيستنفد الخيوط الحاملة. لكن كما يؤكّد الدليل الرسمي، هي ليست «خيوطاً أسرع». سرعة تنفيذ الشيفرة لا تتغيّر — ما توفّره هو المقياس (معدّل الإنتاج).3
هناك ثلاثة ضوابط لاستخدامها.3
- لا تُجمِّعها في بركة. الخيوط الافتراضيّة رخيصة وقابلة للرمي؛ «عدد المهام = عدد الخيوط الافتراضيّة» هو الحالة الصحيحة. وضع خيوط افتراضيّة في
newFixedThreadPoolخطأ — استخدم الشكلtry (var executor = Executors.newVirtualThreadPerTaskExecutor()). - قيِّد التزامن بـ
Semaphore. عبِّر عن قيد مثل «عشرة اتّصالات متزامنة على الأكثر بواجهة خارجيّة» بsemaphore، لا بحجم البركة. - لا تستخدمها لأعمال مرتبطة بالمعالج. خيوط المنصّة بحجم يقارب عدد الأنوية تبقى الأداة المناسبة لموازاة الحساب، كما من قبل.
لاحظ أنّ ما يعمل داخل خيط افتراضي شيفرة تزامنيّة عاديّة. بدل إعادة كتابة شيفرتك كما يفعل async/await في .NET، فلسفة تصميم الخيوط الافتراضيّة أن تدعك تشغّل شيفرة «خيط واحد لكلّ طلب» المستقيمة، دون تغيير، على مقياس هائل.2
3.3. سوء فهم شائع ── «لا حاجة للتجميع» ينطبق فقط على الخيوط الافتراضيّة
لا تقرأ ضابط «لا تُجمِّعها» بمعنى «ليس في Java شيء اسمه بركة خيوط (أو أنّها غير كفؤة)». الواقع عكس ذلك: برك Java جزء ناضج من المكتبة القياسيّة منذ JDK 5 (2004). البركة العامّة القابلة للتهيئة بدقّة ThreadPoolExecutor (تُنشأ عبر مصانع Executors المختلفة)، وForkJoinPool لسرقة العمل (ومثاله المشترك commonPool() هو هدف التنفيذ الافتراضي لـ parallel stream وCompletableFuture)، وScheduledThreadPoolExecutor للتنفيذ الدوري — هذه ما زالت اللاعبين الرئيسيّين لأعمال المعالج.
البركة في جوهرها تحسين قائم على أنّ «إنشاء خيط نظام تشغيل والإمساك به مكلف، لذا أعد استخدامه». تُلغي الخيوط الافتراضيّة هذه المقدّمة بجعل تكلفة الإنشاء قريبة من الصفر، فلم يعد هناك سبب لإعادة استخدامها — الفهم الدقيق ليس أنّ التجميع صار غير كفء، بل أنّ الخيوط صارت خفيفة بما يكفي ليصبح تحسين التجميع غير ضروري. وتحت الخيوط الافتراضيّة، يشغّل مجدول JDK مجموعة خيوط حاملة (خيوط نظام تشغيل) يقارب عددها عدد الأنوية، كـ ForkJoinPool لسرقة العمل.2 بعبارة أخرى، شكل «عالج كمّاً هائلاً من العمل المتزامن ببركة صغيرة من خيوط نظام التشغيل» محفوظ؛ وحده إدارة تلك البركة انتقل من يد المطوّر إلى JVM. من الإنصاف القول إنّ Java تصل إلى الوجهة نفسها التي يصل إليها async/await في .NET، الذي يعيد خيطه إلى البركة عند نقطة await، دون تغيير شكل الشيفرة.
4. تقليل الحالة المشتركة القابلة للتغيير ── التقسيم واللامتغيّر والمجموعات المتزامنة والطوابير
ينشأ التنازع فقط حين يوجد «خيوط متعدّدة» و«بيانات مشتركة قابلة للتغيير» معاً. عدد الخيوط تحدّده المتطلّبات، لذا ما يستطيع التصميم قطعه هو الجزء المشترك. هناك ثلاث عائلات من التقنيات — التقسيم، وجعل الأشياء لامتغيّرة، وتسليم البيانات — وإليك كيف تكتبها في Java.
قسِّمها. للتجميع المتوازي، بدل أن يكتب كلّ خيط إلى متغيّرة مجموع مشتركة، اجعل كلّ خيط يبني نتيجة جزئيّة وادمجها في النهاية. يوفّر reduce / collect في parallel stream هذا الشكل إطاراً، وLongAdder المذكور أدناه كذلك تنفيذ لاستراتيجيّة التقسيم — يشطر داخليّاً إلى خلايا ليوزّع التنازع، ويجمعها عند القراءة. تقليل عدد مرّات الكتابة إلى الحالة المشتركة يأتي قبل كتابة المزامنة صحيحة.
اجعلها لامتغيّرة. ابنِ بيانات بـ record ومجموعات لامتغيّرة (List.copyOf / Map.copyOf) لا تُعاد كتابتها بعد البناء، ويمكنك مشاركتها بلا مزامنة. للإعدادات والبيانات الأساسيّة، النمط القياسي: حين تحتاج استبدالها، ابنِ كائناً جديداً واستبدل مرجعاً volatile. لكن «تبدو للقراءة فقط» و«هي لامتغيّرة» شيئان مختلفان. تُرجع وصولات record المراجع الخام لمكوّناتها، ونسخة List.copyOf أيضاً ضحلة (لا تكرّر كائنات العناصر)، لذا إن كانت العناصر قابلة للتغيير، فمن يملك اسماً مستعاراً يستطيع إعادة كتابة المحتوى، ويبقى التنازع. لا يأمن المشاركة بلا مزامنة إلّا حين يكون رسم الكائنات بالكامل — بما فيه العناصر — لامتغيّراً. إن دخلت عناصر قابلة للتغيير، فإمّا مرِّر نسخة عميقة أو ادفع العناصر أيضاً نحو record / أنواع لامتغيّرة.
استخدم العمليّات المركّبة على المجموعات المتزامنة. لنمط «أنشئه إن غاب، ثمّ أدرجه» في ConcurrentHashMap استخدم computeIfAbsent. هذا المنهج ينفّذ الاستدعاء كلّه ذرّيّاً، وإن غاب المفتاح تُستدعى دالّة الربط مرّة واحدة بالضبط داخل ذلك الاستدعاء الواحد.8 الضمان يختلف عن ConcurrentDictionary.GetOrAdd في .NET (الذي يمكن أن تعمل مصنعه أكثر من مرّة تحت التنازع) — نقطة يخلطها بسهولة من ينتقل بين اللغتين. لكنّه ليس «مرّة واحدة بالضبط على مدى حياة المفتاح». إن أرجعت الدالّة null أو رمت، لا يُسجَّل ربط، وتعمل الدالّة من جديد في استدعاء لاحق (الأمر نفسه إن حُذف الإدخال بعد التسجيل). إن لم يحتمل تهيئتك آثاراً جانبيّة مكرّرة، صمِّمها بحيث تنجح الدالّة وترجع غير null. ثمناً للذرّيّة تُحظر بعض تحديثات الخيوط الأخرى أثناء الحساب، لذا أبقِ دالّة الربط قصيرة، ولا تحدِّث هذه الخريطة نفسها من داخل الدالّة (تحديث تعاودي مكتشف يمكن أن يرمي IllegalStateException).8
// النمط القياسي لعدّاد تواتر: computeIfAbsent + LongAdder
ConcurrentHashMap<String, LongAdder> freqs = new ConcurrentHashMap<>();
freqs.computeIfAbsent(key, k -> new LongAdder()).increment();
سلِّم البيانات عبر طابور. وجِّه تدفّق البيانات بين الخيوط عبر BlockingQueue. مع ArrayBlockingQueue أُعطي سعة، يحظر put حين يمتلئ، فيعطي ضغطاً عكسيّاً طبيعيّاً — الشكل نفسه للقناة المحدودة في طبعة .NET. حتّى في عصر الخيوط الافتراضيّة، يبقى تصميم رسم حدّ واضح بين المنتج والمستهلك فعّالاً.
5. انضباط الأقفال ── synchronized وReentrantLock
5.1. على ماذا تقفل، وماذا لا تفعل وأنت تمسك قفلاً
فكِّر في وحدة القفل لا كـ «قسم شيفرة» بل كـ «بيانات». اربط كائن قفل واحداً بكلّ مجموعة بيانات قابلة للتغيير تريد حمايتها، وخذ ذلك القفل نفسه في كلّ مكان يلمس تلك البيانات — واقع خطأ السباق عادة أنّ هذا الربط انهار في مكان ما. تجنّب synchronized(this) وsynchronized(SomeClass.class)، لأنّ شيفرة خارجيّة تستطيع قفل الكائن نفسه؛ بدلاً من ذلك اقرن البيانات التي تريد حمايتها واحداً لواحد مع private final Object lock = new Object(); لا يُكشف خارجاً أبداً.
ضابطان إضافيّان فوق ذلك. أوّلاً، لا تفعل شيئاً بطيئاً، ولا شيئاً يلمس العالم الخارجي، وأنت تمسك قفلاً. I/O أو استدعاء مستمعين أو تشغيل شيفرة مجهولة وأنت ما زلت تمسك القفل يطيل مدّة الإمساك ويخاطر بأن يحاول المستدعى أخذ قفل آخر، فيُنشئ الانتظار الدائري من الشكل 2. ثانياً، ثبِّت ترتيب الاكتساب لأقفال متعدّدة. حيث تأخذ قفلين أو أكثر، اجعل قاعدة أنّ كلّ خيط يأخذها بالترتيب نفسه، وللأماكن التي لا تستطيع ضمان الترتيب فيها جهِّز مساراً «تخلَّ وأعد المحاولة إن لم تحصله» بـ tryLock(timeout) المذكور أدناه.
synchronized كافٍ لـ «إقصاء قصير بسيط». انتقل إلى ReentrantLock حين تحتاج ما يلي.
- اكتساب مؤقّت عبر
tryLock(timeout)(تحويل تعليقة دائمة إلى فشل تستطيع تسجيله ومعالجته) - سياسات عدل، أو عدّة
Condition، أو حين تريد فصل اكتساب قفل وتحريره عبر مناهج مختلفة
حين تستخدم ReentrantLock، لا تكسر نمط try فوراً بعد lock() وunlock() في كتلة finally (ليس في Java مكافئ لـ RAII في C++، لذا هذا النمط هو الانضباط كلّه).
5.2. الخيوط الافتراضيّة والتثبيت ── ما تغيّر في JDK 24
حين قُدِّمت الخيوط الافتراضيّة أوّل مرّة (JDK 21-23)، كان هناك قيد بأنّ الحظر داخل كتلة synchronized يثبّت الخيط الافتراضي على خيط نظام التشغيل (لا يستطيع تحرير خيط نظام التشغيل، فيفقد فائدة المقياس)، وكان يُنصح باستبدال مواضع الحظر المتكرّرة أو الطويلة بـ ReentrantLock.3 أُزيل ذلك القيد حين أعاد JEP 491 في JDK 24 كتابة تنفيذ المراقب، وصار synchronized لا يثبّت الخيوط الافتراضيّة.4 إن كنت على JDK 24 أو أحدث، فلم يعد الاستبدال الآلي لـ synchronized كإجراء مضادّ للتثبيت ضروريّاً. يستحقّ التحقّق ممّا إذا كانت إرشادات منظّمتك الأقدم ما زالت عالقة عند تحذير عصر JDK 21.
5.3. أين تقع الذرّيّات وvolatile
التحديثات الذرّيّة لمتغيّرة واحدة تتولاها AtomicInteger / AtomicLong / AtomicReference (أو، لإحصاءات تُزاد فقط بتواتر عالٍ، LongAdder المقاوم للتنازع). يضمن volatile الرؤية والترتيب (happens-before)، لا الذرّيّة للعمليّات المركّبة.5 الاستنتاج نفسه في طبعتي .NET وC++ ينطبق في Java أيضاً: استخدم الذرّيّات للرايات والقيم المفردة، والأقفال للحالة المركّبة، ولا تحاول أن تجعل volatile يفعل ذلك وحده.
6. تصميم كيف تتوقّف ── المقاطعة كلغة مشتركة
6.1. آداب interrupt
الإيقاف والإلغاء في Java موحَّدان حول المقاطعة. يضبط t.interrupt() حالة مقاطعة الخيط المستهدف، وإن كان المستهدف محظوراً في sleep / wait / join أو ما شابه، يرمي InterruptedException ليوقظه فوراً (وعندها تُمسح حالة المقاطعة).6 Thread.stop / suspend / resume، الآليّات القسريّة في الماضي، غير آمنة جوهريّاً، لذا يؤدّي استدعاؤها الآن إلى UnsupportedOperationException.6
flowchart TB
OWNER["المستدعي يوقفه - يستدعي t.interrupt"] --> ST["تُضبَط حالة المقاطعة"]
ST --> A["خيط يحسب:<br/>يستطلع Thread.interrupted في حلقة"]
ST --> B["محظور في sleep / wait / join:<br/>يُطلَق InterruptedException ويوقظه فوراً<br/>تُمسح الحالة"]
A --> E["ينظِّف وينهي بنفسه"]
B --> C{"ماذا يفعل catch؟"}
C -->|"يستطيع الإنهاء بنفسه"| E
C -->|"لا يستطيع الإنهاء - مثلاً داخل مكتبة"| R["Thread.currentThread.interrupt<br/>يستعيد الحالة ويترك الإشارة"]
R --> E
الشكل 4: الإيقاف التعاوني عبر المقاطعة. ابتلاع InterruptedException يجعل إشارة التوقّف تختفي — حين تلتقطه فالخيار إمّا «أنهِ» أو «استعد»
هناك ضابط واحد فقط تحتاج تذكّره في الممارسة: لا تكتب شيفرة تلتقط InterruptedException ولا تفعل شيئاً. إن استطعت الإنهاء ضمن مسؤوليّتك فأنهِ هناك؛ وإن لم تستطع فاستعد الحالة بـ Thread.currentThread().interrupt() ومرِّر الإشارة إلى من استدعاك (انظر الأسئلة الشائعة).
6.2. الإيقاف ذو المرحلتين لـ ExecutorService
واجهات إيقاف ExecutorService تقف فوق نموذج المقاطعة. يوقف shutdown() قبول مهام جديدة ويدع المهام المُرسَلة أصلاً تجري حتّى الاكتمال؛ يحاول shutdownNow() إيقاف المهام الجارية. كمواصفة واجهة هذا أفضل جهد، ومُوثَّق صراحة أنّ تنفيذاً قياسيّاً (مثل ThreadPoolExecutor) يُلغي عادة عبر Thread.interrupt() — بمعنى أنّ مهمّة لا تستجيب للمقاطعة لن تتوقّف حتّى مع shutdownNow، وإن كنت تستخدم تنفيذاً مخصَّصاً لـ Executor فعليك مراجعة وثائقه لترى كيف يُلغي (وما إذا كان يرسل مقاطعة أصلاً).1 نمط الإيقاف القياسي الذي تعرضه الوثائق الرسميّة هو النمط ذو المرحلتين التالي.1
/** صحيح متى اكتمل الإيقاف. لا تنتقل إلى تحرير الموارد المشتركة ما دام هذا خاطئاً. */
boolean shutdownAndAwaitTermination(ExecutorService pool) {
pool.shutdown(); // المرحلة 1: أوقف قبول مهام جديدة وانتظر الاكتمال
try {
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
// المرحلة 2: اطلب الإلغاء. المهام التي أُسقطت قبل التشغيل
// تُرجَع، لذا علِّم Future الخاصّ بها ملغاة لإيقاظ من ينتظر get()
pool.shutdownNow().forEach(r -> {
if (r instanceof Future<?> f) f.cancel(false);
});
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("Pool did not terminate");
return false; // لم يكتمل الإيقاف. بلِّغ بشكل يمكن تمييزه عن النجاح
}
}
return true;
} catch (InterruptedException ex) {
pool.shutdownNow().forEach(r -> {
if (r instanceof Future<?> f) f.cancel(false);
});
Thread.currentThread().interrupt(); // استعد حالة مقاطعة هذا الخيط أيضاً
return false; // هذا المسار أيضاً قد يترك الإيقاف ناقصاً
}
}
flowchart TB
S["shutdown<br/>يوقف قبول مهام جديدة"] --> W1{"awaitTermination<br/>ينتظر الاكتمال"}
W1 -->|"ينتهي ضمن المهلة"| DONE["اكتمل الإيقاف"]
W1 -->|"تنتهي المهلة"| NOW["shutdownNow<br/>يرسل مقاطعة للمهام الجارية<br/>الاستجابة أمر يخصّ المهمّة"]
NOW --> W2{"awaitTermination<br/>ينتظر من جديد"}
W2 -->|"يكتمل"| DONE
W2 -->|"ما زال لم ينتهِ"| LOG["سجِّله شذوذاً<br/>اشتبه في مهمّة تتجاهل المقاطعات"]
الشكل 5: الإيقاف ذو المرحلتين لـ ExecutorService. تصميم مرحلي «انتظر بأدب → اطلب عبر المقاطعة → إن لم ينتهِ بعد، راقبه شذوذاً»
هناك حدّ واحد لإلغاء كائنات Runnable التي يرجعها shutdownNow(). ما يعود هو الكائن الذي كان جالساً في طابور التنفيذ — لـ submit عادي هو FutureTask نفسه الذي سُلِّم للمستدعي، لكن للمهام المُرسَلة عبر غلاف مثل ExecutorCompletionService هو الغلاف داخل الطابور، كائن مختلف عن Future المستدعي. في ذلك التكوين لن يُكمل الإلغاء أعلاه Future المستدعي، لذا صمِّم الأمر ليحتفظ بقائمتك من Future عند الإرسال ويُلغي تلك عند الإيقاف (أو يُرجع المهام المُسقَطة إلى مالكها).
close() (AutoCloseable)، المتاح من JDK 19 فصاعداً، يغلِّف «استدعِ shutdown وانتظر الاكتمال» في شكل تستطيع كتابته بـ try-with-resources، وtry (var executor = ...) مع newVirtualThreadPerTaskExecutor للخيوط الافتراضيّة هو الشكل الأساسي الحديث.1 لكن close() ليس بديلاً للنمط ذي المرحلتين أعلاه. لأنّه ينتظر الاكتمال بلا مهلة، إن لم تستجب مهمّة واحدة للمقاطعة أو لم تنتهِ أبداً، يحظر الخيط الذي يحاول إغلاقه إلى الأبد. هو أداة تناسب نطاقاً تكون فيه المهام محدودة ومضموناً اكتمالها (أرسلها هناك، وانتظرها هناك)؛ لأماكن مثل مسار إيقاف تطبيق، حيث تريد أن ينتهي دائماً ضمن زمن محدود، استخدم النمط ذا المرحلتين المؤقَّت بدلاً منه. إلغاء مهمّة فرديّة كذلك يتمّ عبر المقاطعة، بـ Future.cancel(true).
7. خيط الواجهة ── EDT في Swing
تطبيقات سطح المكتب، بغضّ النظر عن اللغة أو الإطار، تتبع قاعدة أنّ الواجهة تخصّ الخيط الذي يديرها حصراً. في Swing، ذلك الخيط الحصري هو Event Dispatch Thread (EDT): مناهج مكوّنات Swing، كقاعدة، ليست آمنة للخيوط، ولمسها من خيوط متعدّدة يدعو تداخل خيوط وأخطاء اتّساق ذاكرة. اطلب تحديثات الشاشة من خيوط أخرى عبر SwingUtilities.invokeLater إلى EDT، وبالعكس، لأنّ تشغيل عمليّات طويلة على EDT يجمد الواجهة، ادفع العمل الثقيل إلى خيط عامل عبر SwingWorker أو ما شابه.7 يتبع JavaFX الشكل نفسه: تُطلب تحديثات الواجهة على خيط التطبيق عبر Platform.runLater.
8. التحقّق والتنقيح ── مقالب الخيوط سلاحاً
لا تتوقّع أن تجد الاختبارات أخطاء السباق. الاختبارات العاديّة تعدّ تشغيلاً لم يحدث فيه تنازع بمحض الصدفة نجاحاً. فكِّر في الدفاع ثلاث طبقات.
خطّ الدفاع الأوّل هو التصميم. في المراجعة، تحقّق بجدول: أيّ بيانات قابلة للتغيير مشتركة، أيّ قفل يحمي كلّ عنصر (الربط من القسم 5.1)، هل ترتيب اكتساب الأقفال فريد، هل تبتلع أيّ كتلة catch InterruptedException، وهل يصل مسار التوقّف (shutdown/المقاطعة) إلى كلّ مهمّة.
ثانياً، أحسن استخدام مقالب الخيوط. لدى Java أداة قياسيّة لالتقاط «حالة كلّ خيط في هذه اللحظة المتجمّدة»: يطبع jstack (أو jcmd <pid> Thread.print) تتبّعات المكدّس، ويضيف الخيار -l معلومات الأقفال أيضاً.9 لاحظ أنّ صيغة المقلب التقليديّة هذه لخيوط المنصّة؛ ولا تتضمّن خيوط تطبيقك الافتراضيّة. حين تتتبّع طلباً محظوراً في تكوين يستخدم خيوطاً افتراضيّة (القسم 3)، استخدم jcmd <pid> Thread.dump_to_file -format=json <file>، الذي يستطيع قلب الخيوط الافتراضيّة أيضاً.2 الإجراء الأساسي للتحقيق في تعليقة هو أخذ مقلبين أو ثلاثة بفاصل ثوانٍ ومقابلة أيّ قفل ينتظره كلّ خيط خامل، ومن يمسك ذلك القفل. إن سجّلت انتهاء مهلة tryLock(timeout) (القسم 5.2) يمكنك حتّى أتمتة محفّز أخذ المقلب.
ثالثاً، هزّ الأشياء تحت الحمل. اختبار الإجهاد — التشغيل طويلاً بتوازي أكثر من عدد الأنوية، وعشوائية ترتيب المعالجة، وحقن تأخيرات اصطناعيّة — سبيل عملي لجعل «إصابة» حالة سباق أكثر احتمالاً على آلة تطوير. نفِّذ اختباراً واحداً على الأقلّ بحجم بيانات وعدد خيوط بحجم الإنتاج قبل الإصدار.
9. إلى أين يتّجه تزامن Java ── التزامن المنظَّم
نظرة سريعة نصف خطوة إلى الأمام، للختام. مبنيّاً على افتراض الخيوط الافتراضيّة، Structured Concurrency (StructuredTaskScope) — الذي يعامل مهاماً فرعيّة متعدّدة كوحدة عمل واحدة وينظِّم انتشار الفشل والإلغاء — قيد التطوير، وحتّى أغسطس 2026 ما زال ميزة معاينة. عُدِّل إلى شكل واجهة مبنيّ على StructuredTaskScope.open() في المعاينة الخامسة لـ JDK 25 (JEP 505)، ويستمرّ إلى معاينة سادسة (JEP 525) في JDK 26 الحالي.1011 في الأثناء، Scoped Values، مشاركة سياق لامتغيّرة تحلّ مشكلات ThreadLocal، أُنجزت في JDK 25.12 مبادئ هذا المقال — حدود مهام واضحة، مشاركة لامتغيّرة، إيقاف تعاوني — تتوافق مع الاتّجاه الذي تسير إليه واجهات البرمجة الجديدة هذه أيضاً.
10. الخلاصة ── قائمة تحقّق Java
- هل بقي أيّ
new Threadفي شيفرة الأعمال (هل بُني علىExecutorService/ الخيوط الافتراضيّة)؟ - هل وُجِّهت أعمال I/O-bound وأعمال المعالج إلى آليّات تنفيذ مختلفة (الفرع في الشكل 3)؟
- هل الخيوط الافتراضيّة غير مجمَّعة في بركة، وهل يُقيَّد التزامن بـ
Semaphore؟ - هل البيانات المشتركة لامتغيّرة (
record/List.copyOf)، أو مبنيّة على أدواتjava.util.concurrent؟ - ألا يوجد
synchronized(this)أو قفل على كائن مكشوف علناً؟ - هل تستخدم العمليّات المركّبة لـ
ConcurrentHashMap(computeIfAbsentوغيرها) وتُبقي دالّة الربط قصيرة؟ - ألا تتوقّع ذرّيّة من
volatile(هل العدّادات تستخدم أصناف Atomic /LongAdder)؟ - ألا توجد كتلة catch واحدة تبتلع
InterruptedException؟ - هل يتبع إيقاف
ExecutorServiceالنمط ذا المرحلتين المؤقَّت (وهل الأماكن التي تستخدمclose()محدودة بنطاقات اكتمال المهام فيها مضمون)؟ - هل تحديثات واجهة Swing/JavaFX مجمَّعة على EDT / خيط التطبيق؟
Java من أكثر اللغات تجهيزاً بأدوات التزامن، وقد فتح وصول الخيوط الافتراضيّة طريقاً لتوسيع شيفرة تزامنيّة مستقيمة دون إعادة كتابتها. لهذا بالضبط فإنّ الإمساك الصحيح بتقسيم العمل بين الأدوات — أيّها لمعدّل الإنتاج، وأيّها للإقصاء، وما الذي يشير إلى التوقّف — هو جوهر تصميم تعدّد الخيوط في Java.
مقالات ذات صلة
- أفضل الممارسات العمليّة لتعدّد الخيوط: طبعة .NET ── ما الذي تقرّره قبل أن تضيف مزيداً من الخيوط
- أفضل الممارسات العمليّة لتعدّد الخيوط: طبعة C++ ── إزالة الحوادث بالبنية مع RAII وjthread
- أفضل الممارسات العمليّة لتعدّد الخيوط: طبعة C ── الكتابة بأمان على طريقة Win32 API
- أفضل الممارسات لـ C# async/await - جدول قرار لـ Task.Run و ConfigureAwait
مجالات الاستشارة ذات الصلة
تتعامل شركة كومورا سوفت ذ.م.م. مع مراجعات تصميم تعدّد الخيوط لأنظمة الأعمال والمعالجة بالدفعات في Java، والتحقيق في السبب الجذري لعيوب التزامن مثل فساد الحالة المشتركة وأعراض «لا يتوقّف أحياناً / يتجمّد» (تحليل مقالب الخيوط)، والاستشارة التقنيّة حول تبنّي الخيوط الافتراضيّة.
روابط مرجعيّة
-
Oracle, ExecutorService (Java SE 21 & JDK 21 API). حول ترك shutdown() المهام المُرسَلة أصلاً تجري حتّى الاكتمال مع وقف الإرسالات الجديدة؛ وحول محاولة shutdownNow() إيقاف المهام الجارية وإرجاع قائمة مهام كانت تنتظر التنفيذ، غير أنّ التنفيذ النموذجي يُلغي عبر Thread.interrupt()، بلا ضمان يتجاوز أفضل جهد، لذا مهمّة لا تستجيب للمقاطعة لن تنتهي؛ وحول إمكان انتظار الاكتمال بـ awaitTermination؛ وحول استدعاء close() (AutoCloseable، من Java 19 فصاعداً) لـ shutdown وانتظار الاكتمال، وقابليّته للاستخدام مع try-with-resources؛ وحول عرض الإيقاف ذي المرحلتين shutdown → awaitTermination → shutdownNow كمثال استخدام. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
OpenJDK, JEP 444: Virtual Threads. حول صيرورة الخيوط الافتراضيّة ميزة رسميّة في JDK 21؛ وحول كونها خيوطاً خفيفة تقلّل كثيراً جهد كتابة تطبيقات متزامنة عالية معدّل الإنتاج وصيانتها ورصدها؛ وحول فلسفة التصميم بتوسيع شيفرة تزامنيّة مستقيمة «طلب واحد، خيط واحد» دون تغيير؛ وحول كون مجدول الخيوط الافتراضيّة في JDK هو ForkJoinPool لسرقة العمل يعمل بوضع FIFO، وتوازيّه الافتراضي يساوي عدد المعالجات المتاحة؛ وحول إضافة صيغة مقلب خيوط جديدة تتضمّن الخيوط الافتراضيّة باسم jcmd Thread.dump_to_file (نصّاً صريحاً وJSON)، وأنّ مقالب الخيوط التقليديّة لا تتضمّن الخيوط الافتراضيّة. ↩ ↩2 ↩3 ↩4 ↩5
-
Oracle Java SE Core Libraries, Virtual Threads. حول كون الخيوط الافتراضيّة خيوطاً خفيفة تنفّذها بيئة تشغيل Java وتحرّر خيط نظام التشغيل أثناء I/O الحاجز؛ وحول كونها ميزة للمقياس (معدّل الإنتاج) لا للسرعة (الكمون)، وأنّها غير مناسبة للمعالجة كثيفة المعالج؛ وحول عدم تجميع الخيوط الافتراضيّة أبداً واستخدام واحدة لكلّ مهمّة (newVirtualThreadPerTaskExecutor)؛ وحول استخدام Semaphore لا بركة خيوط لتقييد التزامن؛ وحول أنّ الحظر داخل synchronized حتّى JDK 21 يسبّب تثبيتاً على خيط نظام التشغيل، ولهذا نُصح باستبدال المواضع المتكرّرة أو الطويلة بـ ReentrantLock؛ وحول إمكان كشف التثبيت بـ -Djdk.tracePinnedThreads. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
OpenJDK, JEP 491: Synchronize Virtual Threads without Pinning. حول إعادة كتابة تنفيذ مراقب JVM في JDK 24 لدعم الخيوط الافتراضيّة، بحيث لم يعد الحظر داخل كتلة أو منهج synchronized يثبّت خيطاً افتراضيّاً على خيطه الحامل؛ وحول أنّ هذا يعني أنّ إجراء عصر JDK 21-23 «استبدال synchronized بـ ReentrantLock» لم يعد ضروريّاً من حيث المبدأ. ↩ ↩2
-
Oracle, The Java Tutorials, Memory Consistency Errors. حول نشوء أخطاء اتّساق الذاكرة حين يكون لخيوط متعدّدة رؤى غير متّسقة للبيانات نفسها؛ وحول أنّ مفتاح تجنّبها هو علاقة happens-before (ضمان أنّ كتابة ذاكرة لجملة مرئيّة لجملة أخرى)؛ وحول أنّ synchronized وvolatile وThread.start / join، من بين غيرها، تُنشئ علاقات happens-before. ↩ ↩2 ↩3
-
Oracle, Thread (Java SE 21 & JDK 21 API). حول كون Thread.stop / suspend / resume غير آمنة جوهريّاً (تُحرَّر الأقفال في حالة غير متّسقة وتصير الكائنات المكسورة مرئيّة؛ ويمكن أن يدعو suspend إلى جمود)، ما جعلها مهملة تمهيداً للحذف، وأنّها ترمي الآن UnsupportedOperationException عند الاستدعاء؛ وحول ضبط interrupt() لحالة المقاطعة وإيقاظ خيط محظور في sleep / wait / join برمي InterruptedException (الذي يمسح حالة المقاطعة)؛ وحول اختلاف كيف يعامل interrupted() وisInterrupted() تلك الحالة. ↩ ↩2 ↩3
-
Oracle, The Java Tutorials, The Event Dispatch Thread. حول تشغيل شيفرة معالجة أحداث Swing على Event Dispatch Thread (EDT)؛ وحول أنّ معظم مناهج كائنات Swing ليست آمنة للخيوط، لذا فإنّ استدعاءها من خيوط متعدّدة يدعو تداخل خيوط وأخطاء اتّساق ذاكرة، بمعنى أنّ الوصول إلى مكوّنات Swing ينبغي، كقاعدة، أن يتمّ على EDT؛ وحول طلب مهام على EDT من خيوط أخرى عبر SwingUtilities.invokeLater / invokeAndWait؛ وحول حاجة المهام التي تعمل على EDT إلى الإنهاء بسرعة. ↩ ↩2
-
Oracle, ConcurrentHashMap (Java SE 21 & JDK 21 API). حول تنفيذ استدعاء منهج computeIfAbsent كلّه ذرّيّاً، مع استدعاء دالّة الربط مرّة واحدة بالضبط حين يغيب المفتاح؛ وحول حظر بعض عمليّات التحديث من خيوط أخرى أثناء الحساب، لذا ينبغي إبقاؤها قصيرة وبسيطة؛ وحول منع دالّة الربط من تعديل هذه الخريطة نفسها، وأنّ تحديثاً تعاوديّاً مكتشفاً ينتج IllegalStateException؛ وحول أنّ عمليّة استرجاع (get) لا تحظر، مع قيام علاقة happens-before بين تحديث لمفتاح معيّن واسترجاع لاحق. ↩ ↩2 ↩3
-
Oracle, The jstack Command (Java SE 21 Tools Reference). حول طباعة jstack تتبّعات المكدّس (اسم الصنف، اسم المنهج، رقم السطر) لكلّ خيط في عمليّة Java محدّدة؛ وحول تمكين الخيار -l عرضاً تفصيليّاً يتضمّن معلومات أقفال إضافيّة؛ وحول استخدامه إلى جانب أدوات تشخيص أخرى مثل jcmd. ↩
-
OpenJDK, JEP 505: Structured Concurrency (Fifth Preview). حول معاملة واجهة التزامن المنظَّم مجموعة مهام فرعيّة مترابطة كوحدة عمل واحدة وتنظيم انتشار الأخطاء والإلغاء؛ وحول تعديل StructuredTaskScope إلى شكل يُفتح عبر منهج مصنع ساكن (open)؛ وحول كونه، حتّى JDK 25، معاينة خامسة وليس ميزة منجَزة بعد. ↩
-
OpenJDK, JEP 525: Structured Concurrency (Sixth Preview). حول استمرار التزامن المنظَّم معاينة سادسة في JDK 26 أيضاً — أي أنّه، حتّى أغسطس 2026، ما زال ميزة معاينة في JDK الحالي، تتطلّب تفعيل ميزات المعاينة لاستخدامه. ↩
-
OpenJDK, JEP 506: Scoped Values. حول إنجاز Scoped Values في JDK 25؛ وحول كونها آليّة لمشاركة بيانات سياق لامتغيّرة بأمان وكفاءة داخل الخيوط وعبرها، موفِّرة حلاً لمشكلات ThreadLocal (القابليّة للتغيير، وإدارة دورة الحياة، وتكلفة التوريث). ↩
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
أفضل ممارسات تعدّد مؤشّرات الترابط عمليّاً: طبعة C ── الكتابة بأمان على طريقة Win32 API
النهج المستقرّ لتعدّد مؤشّرات الترابط في C مع Win32 هو الإنشاء عبر _beginthreadex، وأقفال SRW ومتغيّرات الشرط، ودوال Interlocked، وتصميم ...
أفضل الممارسات العمليّة لتعدّد مؤشّرات الترابط: نسخة C++ ── إزالة الحوادث بالبنية عبر RAII وjthread
في C++، تعدّد مؤشّرات الترابط عالمٌ تصبح فيه سباقات البيانات سلوكاً غير معرَّف. يعالج المقال فخّ مدمِّر std::thread، وتصميم التوقّف بـ jt...
متى لا يُفضَّل تحويل تطبيق Windows إلى الويب: جدول القرار وحل «التقسيم» الواقعي
تتزايد الطلبات على تحويل تطبيقات Windows الداخلية إلى الويب، لكن في التطبيقات التي تعتمد على التكامل مع الأجهزة، ومعالجة الملفات المحلية،...
تطبيقات تتعطّل عند الاستئناف من السكون ── كيف تعمل أحداث الطاقة في Windows وكيف تبني تطبيقات أعمال تصمد أمامها
فتحت الحاسوب المحمول فوجدت اتّصالات تطبيق الأعمال ميّتة ── السبب تصميم لم يحسب حساب السكون. يغطّي المقال تدفّق إشعار WM_POWERBROADCAST، و...
مدخل إلى إمكان الوصول في تطبيقات Windows ── الاستعداد لأتمتة الواجهة ومتطلّبات الترتيبات التيسيرية
على خلفيّة تعديل قانون القضاء على التمييز ضدّ ذوي الإعاقة الذي دخل حيّز التنفيذ في أبريل 2024، ينظِّم المقال التسمية في WinForms/WPF، وتش...
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- إن كانت الخيوط الافتراضيّة موجودة الآن، فهل لم نعد نحتاج بركة خيوط (ExecutorService)؟
- يعتمد على حالة الاستخدام. الخيوط الافتراضيّة آليّة لتشغيل أعداد هائلة من المهام التي تهيمن عليها انتظار I/O؛ فهي لا تجعل الشيفرة أسرع، بل ترفع معدّل الإنتاج. لأعمال I/O-bound استخدم خيطاً افتراضيّاً واحداً لكلّ مهمّة (Executors.newVirtualThreadPerTaskExecutor) ولا تُجمّع الخيوط الافتراضيّة في بركة أبداً. أمّا لموازاة حساب يستنفد المعالج، فما زالت بركة خيوط المنصّة المحدودة تقريباً بعدد الأنوية (أو parallel stream) هي الأداة المناسبة كما من قبل. وإن أردت تقييد عدد الاتّصالات المتزامنة بخدمة خارجيّة، فالتوصية في عصر الخيوط الافتراضيّة هي التقييد بـ Semaphore لا بحجم البركة.
- أيّهما أستخدم، synchronized أم ReentrantLock؟
- للإقصاء القصير البسيط يكفي synchronized، وتبقى الشيفرة موجزة. اختر ReentrantLock حين تحتاج ميزات مثل الاكتساب المؤقّت عبر tryLock، أو سياسة عدل، أو عدّة Condition. هناك تحفظ تاريخي عند الجمع مع الخيوط الافتراضيّة: في JDK 21-23 كانت المشكلة أنّ الحظر داخل كتلة synchronized يثبّت الخيط الافتراضي على خيط نظام التشغيل، لذا كان يُنصح باستبدال مواضع الحظر المتكرّرة أو الطويلة بـ ReentrantLock. أعاد JDK 24 (JEP 491) كتابة تنفيذ المراقب وأزال هذا القيد. من JDK 24 فصاعداً لا تحتاج استبدال synchronized لأسباب التثبيت.
- هل إضافة volatile تجعل شيئاً آمناً للخيوط؟
- لا. يُنشئ volatile في Java علاقة happens-before بين كتابة تلك المتغيّرة وقراءتها، فيضمن الرؤية (أن ترى الخيوط الأخرى أحدث كتابة) والترتيب، لكنّه لا يضمن الذرّيّة لعمليّة مركّبة مثل «اقرأ، احسب، اكتب من جديد». زِد عدّاد volatile int بـ ++ من خيوط متعدّدة فتضيع إضافات. استخدم AtomicInteger / AtomicLong (أو LongAdder للتجميع عالي التواتر) للعدّادات، وقفلاً حين تحمي عدّة متغيّرات معاً. يناسب volatile تقريباً فقط مواقف مثل راية حالة بسيطة — حيث يكتب خيط واحد والآخرون يقرأون فقط.
- أيجوز التقاط InterruptedException وتجاهله؟
- لا. المقاطعة هي إشارة Java القياسيّة للإيقاف والإلغاء، وابتلاعها يُنشئ خيطاً لا يتوقّف. حين يُرمى InterruptedException تكون حالة المقاطعة قد مُسحت أصلاً، فإن لم تستطع إنهاء العمل بنفسك فإمّا أن تستعيد الحالة بـ Thread.currentThread().interrupt() لتترك الإشارة لمن استدعاك، أو تعيد رمي الاستثناء كما هو. كتلة catch فارغة لا تفعل شيئاً سبب كلاسيكي لأخطاء لا يعمل فيها الإيقاف أو يُتجاهل فيها shutdownNow.
- ألا أستطيع إيقاف خيط بـ Thread.stop؟
- لا، لا تستطيع. Thread.stop غير آمن جوهريّاً (يُحرّر الأقفال وهو يتركها في حالة غير متّسقة، فيكشف كائنات مكسورة لخيوط أخرى)، لذا أُهمل منذ زمن طويل، واستدعاؤه في Java الحاليّة يرمي UnsupportedOperationException. الأمر نفسه ينطبق على Thread.suspend / resume. السبيل الشرعي الوحيد لإيقاف خيط هو الإيقاف التعاوني عبر المقاطعة. إن كنت تستخدم ExecutorService فإن shutdown يكتفي بوقف قبول مهام جديدة وانتظار الاكتمال — ولا يرسل مقاطعة للمهام الجارية. الذي يحاول إيقاف المهام الجارية هو shutdownNow، وهو أفضل جهد (التنفيذ القياسي يعمل عبر المقاطعة).
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.