الكلمة المفتاحية synchronized
الكلمة المفتاحية synchronized
في الدرس السابق رأيت كيف تنشأ حالات التسابق عندما تقرأ خيوط متعددة وتكتب في حالة مشتركة دون تنسيق. الجواب الأقدم والأساسي في Java على هذه المشكلة هو الكلمة المفتاحية synchronized. فهمها بعمق — ليس فقط التركيب النحوي، بل ضمانات نموذج الذاكرة والمقايضات في الأداء — أمر ضروري قبل أن تنتقل إلى أدوات التزامن ذات المستوى الأعلى.
المراقِب: القفل المدمج في Java
كل كائن Java يحمل حقلًا خفيًا يُسمى المراقِب (يُعرف أيضًا بـ القفل الجوهري أو قفل الكائن). لا يمكنك رؤية هذا الحقل في كود المصدر؛ تديره JVM. يفرض المراقِب أمرين في آنٍ واحد:
- الاستبعاد المتبادل — خيط واحد فقط يمكنه امتلاك المراقِب في أي لحظة. كل خيط آخر يحاول دخول قسم synchronized على نفس المراقِب يُحجَب حتى يُحرَّر القفل.
- رؤية الذاكرة — عندما يُحرِّر خيط المراقِب، تُدفَع جميع الكتابات التي أجراها إلى الذاكرة الرئيسية. وعندما يكتسب خيط آخر نفس المراقِب لاحقًا، يرى تلك الكتابات. هذا يجعل
synchronizedحدًّا من نوع happens-before.
Class). طريقتان synchronized على نفس النسخة تشتركان في المراقِب ذاته وبالتالي لا يمكن تشغيلهما بالتزامن. أما طريقتان على نسختين مختلفتين فلهما مراقِبان منفصلان ويمكنهما التشغيل بالتزامن.
الدوال المزامَنة (Synchronized Methods)
أضف synchronized إلى تعريف الدالة وستكتسب JVM تلقائيًا مراقِب الكائن this عند الدخول وتُحرِّره عند الخروج — سواء أعادت الدالة قيمة بشكل طبيعي أو رمت استثناءً.
بدون synchronized، يُترجَم count++ إلى ثلاث تعليمات بايتكود (قراءة، جمع، كتابة). تبديل السياق بين أي اثنتين منها في خيط مختلف يتسبب في ضياع تحديث — حالة التسابق الكلاسيكية. جعل الدالتين synchronized يعني أن خيطًا واحدًا فقط يمكنه أن يكون داخل أي منهما في أي وقت.
الكتل المزامَنة: دقة أعلى
تقفل الدالة المزامَنة على this طوال فترة تشغيلها. أما الكتلة المزامَنة فتتيح لك اختيار الكائن الذي تقفل عليه ومقدار الكود الذي تحميه:
بالقفل على Object خاص ونهائي بدلًا من this، تجعل القفل غير مرئي للمستدعين. إذا قفلت على this، يمكن للكود الخارجي أيضًا المزامنة على نفس الكائن وهذا قد يتسبب في تنازع غير متوقع أو توقف تام.
private final Object lock = new Object(); بدلًا من القفل على this أو الفئة. يمنع ذلك تدخل المستدعين ويجعل استراتيجية القفل صريحة وواضحة.
الدوال الساكنة المزامَنة
عندما تكون الدالة static وsynchronized في آنٍ واحد، يكون المراقِب المستخدَم هو كائن Class، لا أي نسخة. هذا القفل مشترك بين جميع نسخ الفئة.
القفل على كائن Class أشمل من القفل على نسخة. احرص ألا تخلط بين المزامنة الساكنة ومزامنة النسخة إذا كانتا تحميان نفس الحالة — فهما يستخدمان مراقِبين مختلفين ولا يوفران استبعادًا متبادلًا بينهما.
إعادة الدخول (Reentrancy)
مراقِبات Java قابلة لإعادة الدخول: الخيط الذي يمتلك بالفعل قفلًا يمكنه اكتسابه مجددًا دون أن يُحجَب. هذا يمنع الخيط من توقيف نفسه عندما تستدعي دالة مزامَنة دالة مزامَنة أخرى على نفس الكائن.
عندما تستدعي set الدالةَ audit، يُعيد نفس الخيط دخول المراقِب الذي يمتلكه بالفعل. تحتفظ JVM بعداد دخول لكل خيط؛ يُحرَّر القفل فقط عندما يصل العداد إلى الصفر.
الأداء والمقايضات
كل نقطة دخول synchronized هي اختناق محتمل: جميع الخيوط تتنافس على نفس القفل وخيط واحد فقط يتقدم في كل مرة. فكر في هذه الاستراتيجيات لتقليل التنازع:
- اجعل الأقسام الحرجة قصيرة. انقل عمليات الإدخال/الإخراج أو استدعاءات الشبكة أو الحسابات الثقيلة خارج الكتلة المزامَنة.
- استخدم أقفالًا منفصلة للحالات المستقلة. إذا لم يُتاح وصول إلى حقلين معًا في أي وقت، فاحمِهما بكائني قفل مختلفين ليتوازى الخيوط في العمل عليهما.
- فكّر في قفل القراءة/الكتابة. يسمح
ReentrantReadWriteLockبقراءة متزامنة لعدد كبير من الخيوط حين لا يوجد كاتب — مكسب كبير في الأعباء الكثيفة القراءة. - فكّر في المتغيرات الذرية. للعدادات المتغيرة الواحدة، تقدم
AtomicLongأوLongAdderإنتاجية غير محجوبة تفوقsynchronized.
synchronized غير المتنازع عليها رخيصة جدًا — في الغالب بضع نانوثانية فقط. التنازع هو التكلفة الحقيقية؛ قللها بتضييق الأقسام الحرجة وتقسيم الأقفال.
الخلاصة
الكلمة المفتاحية synchronized هي الأداة الأساسية للاستبعاد المتبادل في Java. كل كائن له مراقِب؛ اكتسابه يضمن الاستبعاد ورؤية الذاكرة معًا. تقفل الدوال المزامَنة على this؛ أما الكتل المزامَنة فتتيح لك اختيار كائن القفل والنطاق الدقيق. فضّل كائنات القفل الخاصة، واجعل الأقسام الحرجة ضيقة قدر الإمكان، وتذكر أن كل قراءة مزامَنة ضرورية مثلما كل كتابة مزامَنة ضرورية. في الدرس التالي سنتناول volatile — أداة أخف تمنح الرؤية دون الاستبعاد.