المتغيرات الذرية
المتغيرات الذرية
في الدروس السابقة رأيت كيف أن عداد int بسيط مشتركًا بين الخيوط يُنتج نتائج خاطئة لأن تسلسل القراءة-التعديل-الكتابة ليس عملية ذرية. رأيت أيضًا أن synchronized يحل المشكلة، لكنه يفرض ثمنًا: يُجبر جميع الخيوط على المرور تسلسليًا عبر قفل واحد، مما قد يتحول إلى عنق زجاجة عند ازدحام الخيوط.
تُقدّم Java حلًا وسطًا من خلال حزمة java.util.concurrent.atomic: متغيرات آمنة للخيوط خالية من الأقفال، تعتمد على تعليمات ذرية على مستوى المعالج — تحديدًا عملية المقارنة والتعيين (CAS) — بدلًا من mutex. يغطي هذا الدرس أهم أعضاء تلك الحزمة ومتى ولماذا تلجأ إليها.
عملية المقارنة والتعيين (CAS)
CAS هي تعليمة واحدة في المعالج تؤدي ثلاثة أشياء بشكل ذري:
- قراءة القيمة الحالية لموقع في الذاكرة.
- مقارنتها بقيمة متوقعة.
- إن تطابقتا، كتابة قيمة جديدة؛ وإلا لا تفعل شيئًا.
تُعيد العملية ما إذا كانت عملية الاستبدال نجحت. الخيط الخاسر يدور في حلقة — يعيد المحاولة — حتى يفوز. ولأن الفحص والاستبدال يحدثان في تعليمة واحدة من المعالج، لا يستطيع أي خيط آخر التدخل بينهما.
AtomicInteger — العمود الفقري
تُغلّف AtomicInteger قيمة int وتُتيح كل عملية تعديل شائعة كعملية ذرية. أهم الدوال:
get()/set(int)— قراءة أو كتابة مع رؤية كاملة للذاكرة.incrementAndGet()— تضيف 1 ذريًا وتُعيد القيمة الجديدة.getAndIncrement()— تضيف 1 ذريًا وتُعيد القيمة القديمة (مثلi++).addAndGet(int delta)— تضيفdeltaذريًا وتُعيد القيمة الجديدة.compareAndSet(int expected, int update)— تُنفّذ CAS الخام؛ تُعيدtrueعند النجاح.updateAndGet(IntUnaryOperator)— تُطبّق تعبيرًا لامدا ذريًا (Java 8+).
إليك نفس العداد الذي رأيته يتعطل سابقًا، مكتوبًا الآن بـ AtomicInteger:
لا synchronized، لا Lock، ومع ذلك النتيجة دائمًا 200 000 بالضبط.
حلقة CAS اليدوية باستخدام compareAndSet
تُعدّ compareAndSet اللبنة الأساسية لمنطق أكثر تعقيدًا خالٍ من الأقفال. لنفترض أنك تريد تحديد سقف للعداد بشكل ذري:
compareAndSet. إن فشل CAS، فخيط آخر غيّر القيمة بين قراءتك وكتابتك، فابدأ من جديد. للعمليات الحسابية على الذاكرة، نادرًا ما تعمل حلقة إعادة المحاولة أكثر من مرة في ظل ازدحام واقعي.
بقية عائلة المتغيرات الذرية
تحتوي الحزمة على نظائر لأنواع بدائية أخرى وللمراجع:
AtomicLong— نفس واجهةAtomicIntegerلكن لنوعlong.AtomicBoolean— مفيدة لعلامة تقرأها خيوط كثيرة لكن لا يجب أن يُقلبها إلا خيط واحد.AtomicReference<V>— CAS على مرجع كائن؛ مثالية لتبديل لقطة غير قابلة للتغيير بشكل ذري.AtomicIntegerArray،AtomicLongArray،AtomicReferenceArray<E>— وصول ذري لكل عنصر داخل مصفوفة.
تُستخدم AtomicBoolean شائعًا كعلامة تُنفَّذ مرة واحدة فقط:
LongAdder — للعدادات عالية الإنتاجية
تحت ازدحام شديد، تتسبب الخيوط الكثيرة المتنافسة على AtomicLong واحدة في ظاهرة ارتداد سطر الكاش — كل CAS ناجح يُبطل سطر الكاش في كل نواة معالج أخرى، مما يُجبر الخيوط على إعادة المحاولة. قدّمت Java 8 الصنف LongAdder (وDoubleAdder) لحل هذا: يحتفظ داخليًا بـ خلية لكل خيط ويجمعها فقط عند استدعاء sum(). الكتابات لا تتنافس أبدًا؛ القراءات تدفع تكلفة تجميع صغيرة.
LongAdder ليست بديلًا مباشرًا لـ AtomicLong. لا تدعم compareAndSet، وsum() لا تُعطي لقطة متسقة إن كانت الزيادات لا تزال تحدث بشكل متزامن. استخدم LongAdder حين تحتاج مجرد مجموع متراكم، واستخدم AtomicLong حين تحتاج القراءة والتحديث الشرطي في خطوة واحدة.
AtomicReference ومشكلة ABA
لـ CAS على المراجع مشكلة خفية: إن تغيّر مرجع من A إلى B ثم عاد إلى A، فسينجح CAS الذي يتوقع A رغم أن الكائن استُبدل في المنتصف. هذه هي مشكلة ABA. الحل هو AtomicStampedReference<V>، الذي يقرن المرجع بـ ختم صحيح (عداد إصدار) — يجب أن يتطابق المرجع والختم معًا لكي ينجح CAS.
متى تختار المتغيرات الذرية بدلًا من synchronized
- استخدم المتغيرات الذرية حين تكون العملية المحمية تحديثًا حسابيًا أو تحديث مرجع واحد. إنها أبسط وأسرع تحت ازدحام منخفض إلى متوسط.
- استخدم
synchronizedأوReentrantLockحين تحتاج إلى حماية تسلسل متعدد الخطوات كوحدة واحدة (مثلًا، التحقق من شرط وتحديث حقلين معًا). - استخدم
LongAdderحين لديك عداد زيادة بحت تحت حمل متوازٍ شديد ولا تحتاج دلالات CAS.
الخلاصة
تستبدل المتغيرات الذرية الأقفال لتحديثات المتغير الواحد باستخدام تعليمة CAS على مستوى المعالج. تُغطي AtomicInteger وAtomicLong العمليات الحسابية؛ تتعامل AtomicBoolean مع العلامات؛ تبدّل AtomicReference مؤشرات الكائنات بأمان. حلقة الدوران وإعادة المحاولة — اقرأ، احسب، compareAndSet، كرّر عند الفشل — هي النمط الشامل لبناء منطق خالٍ من الأقفال. للعدادات ذات الحجم الكبير، تذهب LongAdder أبعد بإزالة التنافس من خلال خلايا لكل خيط. اختر الأداة المناسبة لحجم العملية: متغيرات ذرية للمنطق أحادي المتغير، وأقفال للقسم الحرج متعدد الخطوات.