مشروع: عداد آمن للخيوط
مشروع: عداد آمن للخيوط
على مدار هذا الفصل الدراسي استعرضت كل أداة أساسية في عالم التزامن: الخيوط، والمزامنة، وكلمة volatile، والمتغيرات الذرية، وأوامر wait/notify، والأقفال المتعثرة. يربط هذا الدرس الختامي كل شيء معًا من خلال بناء عداد آمن للخيوط صحيح وجاهز للإنتاج — نبدأ بنسخة مكسورة ساذجة، ثم نتطور عبر تصاميم متعاقبة أفضل، مقارنين المقايضات في كل مرحلة، حتى نصل إلى الاختيار الصحيح لكل سياق.
لماذا العداد مثال دراسي مثالي؟
يبدو العداد بسيطًا للغاية — مجرد عدد صحيح يزداد. بيد أن كل خطر في التزامن درسناه في هذا الفصل يظهر في تنفيذه: شروط التسابق، وفجوات رؤية الذاكرة، وضياع التحديثات، والإفراط في المزامنة الذي يُعيق الإنتاجية. إصلاح كل مشكلة على حدة أمر تعليمي؛ أما إصلاحها جميعًا معًا فهو هندسة حقيقية.
النسخة الأولى: العداد المعطوب
ابدأ بالنهج الواضح والخاطئ حتى ترى بدقة ما يكسر:
تُترجَم عبارة count++ إلى ثلاث تعليمات في رمز البايت: اقرأ القيمة الحالية، أضف 1، اكتب النتيجة. يمكن لخيطين أن يقرآ نفس القيمة القديمة معًا، ويحسبا نفس النتيجة، ثم يكتبانها — مما يُضيّع تحديثًا واحدًا. نفّذها مع 10 خيوط تُنفّذ كل منها 100 000 عملية زيادة، وستجد النتيجة النهائية نادرًا ما تساوي 1 000 000.
النسخة الثانية: synchronized — صحيحة لكن خشنة
ضع كلمة synchronized على كلا التابعين باستخدام نفس المراقب:
هذا التنفيذ صحيح. يضمن القفل الداخلي على this الاستبعاد المتبادل ورؤية مُتسقة للذاكرة عبر علاقة الحدوث-قبل. كما أنه الكود الأوضح — أي قارئ يفهم العقد فورًا.
التكلفة هي التنافس: كل استدعاء لـincrement() يحجب كل الخيوط الأخرى، حتى حين تكون القراءات أكثر بكثير من الكتابات. لعداد عام بسيط هذا مقبول. أما لعداد عالي الإنتاجية مشترك بين مئات الخيوط فقد يُصبح نقطة اختناق.
النسخة الثالثة: AtomicInteger — صحيحة وسريعة
استبدل حقل int بـAtomicInteger من java.util.concurrent.atomic:
يستخدم AtomicInteger تعليمة CPU تُسمى Compare-And-Swap (CAS) بدلًا من mutex. تحاول CAS تحديث القيمة فقط إذا كانت لا تزال تساوي القيمة المتوقعة؛ فإن غيّرها خيط آخر أولًا، تفشل CAS وتُعيد الحلقة المحاولة. لا يوجد حجب، ولا تبديل سياق، ولا خيط "متوقف" ينتظر قفلًا. تحت التزامن العالي هذا أسرع بكثير من synchronized.
synchronized فمتشائمة — تحجب الجميع مسبقًا. للأقسام الحرجة القصيرة مع خيوط كثيرة، تفوز CAS المتفائلة. للأقسام الحرجة الطويلة، قد يكون القفل المتشائم أفضل لأن حلقات CAS تُهدر وقت المعالج.
النسخة الرابعة: LongAdder — أقصى إنتاجية
حين تحتاج فقط المجموع النهائي ولا تحتاج لقراءة متسقة أثناء العد (كالمقاييس وعدادات الزيارات ومحددات المعدل)، يكون LongAdder أسرع:
يحتفظ LongAdder بمصفوفة خلايا — يُحدّث كل خيط معالج خليته الخاصة عادةً، مما يُقلّل التنافس على CAS إلى ما يقارب الصفر. يجمع sum() كل الخلايا معًا. المقايضة: sum() ليست ذرية نسبةً لاستدعاءات increment() المتزامنة، لذا قد تقرأ مجموعًا قديمًا بعض الشيء. للعدادات التي تكفي فيها الصحة النهائية (عدد مشاهدات الصفحة، مقاييس الإنتاجية) هذا هو الخيار الصحيح.
النسخة الخامسة: فئة حساب مصرفي آمنة للخيوط
تحتاج الأنظمة الحقيقية لثوابت أغنى. فيما يلي حساب مصرفي يمنع الأرصدة السالبة باستخدام synchronized مع عمليات مركبة:
اختيار التنفيذ المناسب
- عداد بسيط قليل التنافس: توابع
synchronized— أوضح كود وأسهل تفكيرًا. - زيادة/نقصان عالي التنافس مع الحاجة لقراءات متسقة:
AtomicInteger/AtomicLong— غير محجوب وسريع. - تراكم صافٍ والقراءات التقريبية مقبولة:
LongAdder— أعلى إنتاجية للمسارات الساخنة كالمقاييس. - ثوابت مركبة (تحقق-ثم-تصرف، تحديثات متعددة الحقول): كتل
synchronized— الذرية تمتد عبر حقول/فحوصات متعددة لا تستطيع المتغيرات الذرية التعبير عنها.
ربط كل شيء معًا: عرض قابل للتشغيل
الخلاصة
العداد الآمن للخيوط هو تجسيد مصغّر لكل برمجة تزامنية: عليك تحديد الحالة المتغيرة المشتركة، واختيار استراتيجية المزامنة الصحيحة، والتفكير في العمليات المركبة. أصبح بحوزتك الآن أربع أدوات — synchronized، وAtomicInteger، وLongAdder، والقفل متعدد الكائنات المرتّب — لكل منها حالة استخدام واضحة. اختر الأبسط التي تلبي متطلبات الصحة والأداء، ووثّق الثوابت في الكود، وستكتب Java متزامنة صحيحة وسريعة معًا.