محتويات الدورة

Java / المتقدم / الدرس 13

نموذج ذاكرة Java والحالة المشتركة

استدل بشأن الرؤية والذرية والترتيب بعلاقات happens-before الموثقة لا التوقيت أو الحظ أو افتراضات العتاد.

وقت القراءة التقريبي: 4 دقيقةتاريخ مراجعة المحتوى
في هذه الصفحة

أهداف الدرس

  • تحديد سباق الحالة وثابته.
  • استخدام المزامنة أو الذرية المناسبة.
  • شرح ضمان النشر والبدء والانضمام.

افصل الرؤية والذرية والترتيب

تحتاج الشيفرة المتزامنة أكثر من حساب تسلسلي صحيح. تتعلق الرؤية بملاحظة كتابة خيط آخر، والذرية بمنع تداخل العملية، والترتيب بقيود العلاقات بين الأفعال. تقرأ count++ وتجمع وتكتب فلا تكون ذرية لمجرد أن count نوعه int. قد يقرأ خيطان القيمة القديمة ويفقدان زيادة.

يحدد نموذج الذاكرة المشاهدات المسموحة بعلاقة happens-before، ولا يعد بتنفيذ كل سطر بترتيبه على كل معالج. استخدم أدوات موثقة لا تجربة مصحح أو نوم يخفي السباق. حدد الثابت وكل قارئ وكاتب أولاً.

احم العملية ببروتوكول قفل واحد

توفر synchronized استبعاداً متبادلاً ورؤية بين فك قفل ومسك لاحق للمراقب نفسه. يجب أن يتبع كل وصول في الثابت البروتوكول نفسه. لا يكفي قفل الكاتب مع قارئ بلا تنسيق. تحتاج ثوابت عدة حقول غالباً مقطعاً واحداً لا حماية مستقلة لكل حقل.

ينفذ المثال ألفي زيادة بدوال متزامنة وينضم للعاملين قبل القراءة فينتج2000 دون نوم. يتاح العداد قبل بدء الخيط ويستخدم كل تعديل مراقبه. قصّر المقاطع وتجنب متعاوناً مجهولاً أو محجوباً تحت القفل إلا بتصميم يقتضيه.

java عداد متزامن بعاملين مكتملين
public class Main {
static final class Counter {
 private int n;
 synchronized void increment() { n++; }
 synchronized int value() { return n; }
}
 public static void main(String[] args) throws Exception {
Counter c=new Counter();
Runnable work=()->{ for(int i=0;i<1000;i++) c.increment(); };
Thread a=new Thread(work),b=new Thread(work);
a.start(); b.start(); a.join(); b.join();
System.out.println(c.value());
 }
}
الناتج المتوقع
2000

تمسك كل زيادة المراقب نفسه وينتظر join العاملين ويلتزم القارئ بالبروتوكول.

استخدم عملية ذرية مركبة عند ملاءمتها

توفر AtomicInteger عمليات تجمع القراءة والتعديل والكتابة ذرياً مثل incrementAndGet. يزيد العاملان500 و700 فينتج1200. تعيد get ثمset المنفصلتان السباق. ولا يجعل حقل ذري عدة حقول حالة عمل ذرية واحدة.

قد تدعم المقارنة والإبدال انتقالاً متفائلاً، لكن يجب أن تتسامح دوال التحديث مع إعادة التقييم حيث تسمح الواجهة. لا تضع أثراً بعيداً في دالة قد تتكرر. اختر المزامنة عندما توضح ثابتاً متعدد المتغيرات؛ ولا تعني بلا قفل أبسط أو أسرع أو صحيحاً دائماً.

java عملية قراءة وتعديل وكتابة ذرية
public class Main {

 public static void main(String[] args) throws Exception {
var count=new java.util.concurrent.atomic.AtomicInteger();
Thread a=new Thread(()->{for(int i=0;i<500;i++) count.incrementAndGet();});
Thread b=new Thread(()->{for(int i=0;i<700;i++) count.incrementAndGet();});
a.start(); b.start(); a.join(); b.join();
System.out.println(count.get());
 }
}
الناتج المتوقع
1200

تحفظ الزيادات1200 ويطبع البرنامج بعد انتهاء العاملين.

انشر الحالة المكتملة بعلاقة حقيقية

تسبق الأفعال قبل بدء الخيط أفعاله وفق happens-before، ويلاحظ join الناجح أفعال الخيط المكتمل حسب القاعدة. توفر مهام Executor وجلب Future علاقات اتساق موثقة أيضاً. تتجاوز هذه العقود ملاحظة أن العامل ينتهي عادة قبل println.

تنشئ volatile رؤية وترتيباً لوصولها، لكن volatile count++ تبقى عملية مركبة متسابقة. لا يؤمن المرجع volatile حقول الكائن أو عمليات مجموعته تلقائياً. تبسط الحالة الثابتة النشر إذا اكتمل بناؤها دون تسريب this؛ وتظل المراجع المتغيرة بحاجة إلى مراجعة.

استخدم الاختبارات دليلاً دون ادعاء إثبات شامل

اضبط التداخل المهم بمزالج أو حواجز أو مزامنة محددة مع مهلة كلية. قد يكشف التكرار سباقاً ولا يثبت غياب كل تنفيذ سيئ مسموح. لا تنسب خرجاً ثابتاً مختلقاً لمثال متسابق؛ فسّر التداخل الخاطئ مباشرة.

راجع الملكية والنشر والثوابت والتنظيف قبل الإنتاجية، ثم قس التنازع والموارد بتزامن ممثل. فضّل وضوح حصر الحالة في خيط أو رسائل ثابتة إذا بسّط الصحة. لا تغير الخيوط الافتراضية نموذج الذاكرة ولا تصلح السباق؛ تنطبق العقود على كل تنفيذ.

الأخطاء الشائعة وإصلاحها

Lost increments

لماذا يحدث؟
لم تكن القراءة والتعديل والكتابة ذرية.
كيف تصلحه؟
استخدم بروتوكول قفل أو عملية ذرية مناسبة.
كيف تتجنب تكراره؟
راجع كل وصول واختبر تصادماً مضبوطاً.

volatile did not protect a multi-step update

لماذا يحدث؟
خُلطت الرؤية بذرية الخطوات.
كيف تصلحه؟
احم الثابت كاملاً لا وصول حقل فقط.
كيف تتجنب تكراره؟
افصل الخصائص في المراجعة.

A reader sees partially initialized state

لماذا يحدث؟
تسرب الكائن قبل اكتمال البناء أو النشر الآمن.
كيف تصلحه؟
أكمل البناء وانشر بحد موثق.
كيف تتجنب تكراره؟
تجنب تسريب this من الباني.

اختبر فهمك

1. يقرأ خيطان count=0 ثم يكتب كل منهما count+1. ما النتيجة الممكنة؟

اعرض الحل المشروح

قد يكتبان1 فتفقد زيادة بدلاً من2. استخدم incrementAndGet أو قفلاً مشتركاً؛ ولا تجمع volatile وحدها العمليات.

2. يجب تغيير الرصيد والإصدار معاً. هل تكفي AtomicInteger لكل منهما؟

اعرض الحل المشروح

لا وحدهما؛ قد يرى خيط حقلاً جديداً وآخر قديماً. احم الزوج ببروتوكول واحد أو استبدل حالة ثابتة تضمهما ذرياً.

أهم ما تعلمته

  • happens-before عقد لا تخمين وقت.
  • لا تحمي ذرية الحقل ثوابت متعددة تلقائياً.
  • قد يبسط الحصر والثبات التزامن.

المراجع الرسمية