مقدمة
في بعض الأحيان ، إذا كنت تستخدم التزامن فقط لقراءة وكتابة حقول مثيل أو اثنين ، فإنه يبدو مكلفًا للغاية. توفر الكلمة الرئيسية المتطايرة آلية خالية من القفل للوصول المتزامن إلى حقول المثيل. إذا تم الإعلان عن المجال على أنه متقلب ، فإن المترجم والجهاز الظاهري يعلم أنه يمكن تحديث المجال بشكل متزامن بواسطة مؤشر ترابط آخر. قبل الحديث عن الكلمة الرئيسية المتقلبة ، نحتاج إلى فهم المفاهيم ذات الصلة لنموذج الذاكرة والخصائص الثلاث في البرمجة المتزامنة: الذرة والرؤية والنظام.
1. نموذج ذاكرة جافا مع الذرة والرؤية والنظام
ينص نموذج ذاكرة Java على وجود جميع المتغيرات في الذاكرة الرئيسية ، وكل مؤشر ترابط له ذاكرة العمل الخاصة به. يجب إجراء جميع عمليات مؤشر ترابط على متغير في الذاكرة العاملة ، ولا يمكن العمل مباشرة على الذاكرة الرئيسية. ولا يمكن لكل مؤشر ترابط الوصول إلى الذاكرة العاملة للمواضيع الأخرى.
في جافا ، قم بتنفيذ البيان التالي:
int i = 3 ؛
يجب أن يقوم مؤشر ترابط التنفيذ أولاً بتعيين خط ذاكرة التخزين المؤقت حيث يوجد المتغير I في مؤشر ترابط العمل الخاص به ، ثم كتابته إلى الذاكرة الرئيسية. بدلاً من كتابة القيمة 3 مباشرة في الذاكرة الرئيسية.
فما هي الضمانات التي توفرها لغة جافا نفسها للذرية والرؤية والنظام؟
الذرة
عمليات القراءة والتعيين للمتغيرات من نوع البيانات الأساسية هي العمليات الذرية ، أي أن هذه العمليات لا يمكن مقاطعة وتنفيذها أو لا.
لنلقي نظرة على الكود التالي:
x = 10 ؛ // بيان 1y = x ؛ // بيان 2x ++ ؛ // بيان 3x = x + 1 ؛ // بيان 4
البيان 1 فقط هو عملية ذرية ، ولا توجد أي من العبارات الثلاثة الأخرى هي العمليات الذرية.
البيان 2 يحتوي في الواقع على 2 عملية. يحتاج أولاً إلى قراءة قيمة x ، ثم كتابة قيمة x إلى الذاكرة العاملة. على الرغم من أن العمليتين لقراءة قيمة X وكتابة قيمة X للذاكرة العاملة هما العمليات الذرية ، إلا أنهما ليسا عمليات ذرية معًا.
وبالمثل ، يتضمن X ++ و X = X+1 3 عمليات: اقرأ قيمة X ، وتنفيذ عملية إضافة 1 ، وكتابة القيمة الجديدة.
وبعبارة أخرى ، فإن القراءة والتعيين البسيطة فقط (ويجب تعيين الرقم لمتغير ، والتعيين المتبادل بين المتغيرات ليس عملية ذرية) هي عملية ذرية.
هناك العديد من الفصول في java.util.concurrent.Atomic الحزمة التي تستخدم تعليمات فعالة على مستوى الماكينة (بدلاً من الأقفال) لضمان ذرة العمليات الأخرى. على سبيل المثال ، توفر فئة AtomicInteger الأساليب Guyrementandget و GrepmentAndget ، والتي تزيد على التوالي وتقليل عدد صحيح بطريقة ذرية. يمكن استخدام فئة AtomicInteger بأمان كعداد مشترك دون مزامنة.
بالإضافة إلى ذلك ، تحتوي هذه الحزمة أيضًا على فصول ذرية مثل AtomicBoolean و Atomiclong و AtomicReference لمبرمجي النظام الذين يقومون بتطوير الأدوات المتزامنة فقط ، ويجب ألا يستخدم مبرمجي التطبيق هذه الفئات.
الرؤية
تشير الرؤية إلى الرؤية بين المواضيع ، والحالة المعدلة لخيط واحد مرئي لخيط آخر. هذا هو نتيجة تعديل الخيط. وسيتم رؤية موضوع آخر على الفور.
عندما يتم تعديل متغير مشترك بواسطة متقلبة ، فإنه يضمن تحديث القيمة المعدلة إلى الذاكرة الرئيسية على الفور ، لذلك يكون مرئيًا للمواضيع الأخرى. عندما تحتاج مؤشرات الترابط الأخرى إلى القراءة ، ستقرأ القيمة الجديدة في الذاكرة.
ومع ذلك ، لا يمكن للمتغيرات المشتركة العادية ضمان الرؤية ، لأنه غير مؤكد عندما يتم كتابة المتغير المشترك العادي إلى الذاكرة الرئيسية بعد تعديله. عندما تقرأها مؤشرات الترابط الأخرى ، قد لا تزال القيمة القديمة الأصلية في الذاكرة ، لذلك لا يمكن ضمان الرؤية.
منسم
في نموذج ذاكرة Java ، يُسمح للمترجمين والمعالجات بإعادة ترتيب التعليمات ، لكن عملية إعادة الترتيب لن تؤثر على تنفيذ البرامج المفردة ، ولكنها ستؤثر على صحة التنفيذ المتزامن متعدد الخيوط.
يمكن استخدام الكلمة الرئيسية المتطايرة لضمان "خط ترتيب" معين. بالإضافة إلى ذلك ، يمكن استخدام متزامن وقفل لضمان الطلب. من الواضح أن المزامنة والقفل تضمن أن هناك مؤشر ترابط ينفذ رمز التزامن في كل لحظة ، وهو ما يعادل ترك مؤشرات الترابط تنفيذ رمز التزامن بالتسلسل ، مما يضمن الطلب بشكل طبيعي.
2. الكلمات الرئيسية المتطايرة
بمجرد تعديل متغير مشترك (متغيرات عضو الفئة ، متغيرات الأعضاء الثابتة) ، فإنه يحتوي على طبقتين من الدلالات:
دعونا نلقي نظرة على قطعة من الكود أولاً. إذا تم تنفيذ الموضوع 1 أولاً وتم تنفيذ الموضوع 2 لاحقًا:
// thread 1Boolean stop = false ؛ بينما (! توقف) {dosomething () ؛} // thread 2stop = true ؛ قد يستخدم الكثير من الناس طريقة الترميز هذه عند مقاطعة الخيوط. ولكن في الواقع ، هل سيعمل هذا الرمز بشكل صحيح تمامًا؟ هل ستتم مقاطعة الموضوع؟ ليس بالضرورة. ولعل في معظم الوقت ، يمكن لهذا الرمز مقاطعة مؤشرات الترابط ، ولكنه قد يتسبب أيضًا في عدم توقف مؤشر الترابط (على الرغم من أن هذا الاحتمال صغير جدًا ، بمجرد حدوث ذلك ، فإنه سيؤدي إلى حلقة ميتة).
لماذا من الممكن التسبب في فشل الخيط في المقاطعة؟ كل مؤشر ترابط له ذاكرة العمل الخاصة به أثناء عملية التشغيل. عند تشغيل الموضوع 1 ، سيقوم بنسخ قيمة متغير الإيقاف ووضعه في ذاكرته العاملة. ثم عندما يغير الموضوع 2 قيمة متغير STOP ، ولكن لم يكن لديه وقت لكتابتها إلى الذاكرة الرئيسية ، فإن الموضوع 2 يذهب للقيام بأشياء أخرى ، ثم لا يعرف الموضوع 1 عن تغييرات الموضوع 2 إلى متغير الإيقاف ، لذلك سيستمر في الحلقة.
ولكن بعد التعديل مع متقلبة يصبح الأمر مختلفًا:
هل يضمن التقلل الذرة؟
نحن نعلم أن الكلمة الرئيسية المتطايرة تضمن رؤية العمليات ، ولكن هل يمكن أن تضمن التقلب أن العمليات على المتغيرات ذرية؟
اختبار الفئة العامة {public platile int inc = 0 ؛ زيادة الفراغ العام () {inc ++ ؛ } public static void main (string [] args) {final test test = new test () ؛ لـ (int i = 0 ؛ i <10 ؛ i ++) {new thread () {public void run () {for (int j = 0 ؛ j <1000 ؛ j ++) test.increase () ؛ } ؛ }.يبدأ()؛ } // تأكد من أن مؤشرات الترابط السابقة قد أكملت التنفيذ بينما (thread.activecount ()> 1) thread.yield () ؛ system.out.println (test.inc) ؛ }} نتيجة هذا الرمز غير متناسق في كل مرة تعمل فيها. إنه رقم أقل من 10000. كما ذكرنا سابقًا ، فإن عملية التكرار التلقائي ليست ذرية. ويشمل قراءة القيمة الأصلية للمتغير ، وإجراء عملية واحدة إضافية ، والكتابة إلى الذاكرة العاملة. وهذا يعني ، يمكن تنفيذ العمليات الفرعية الثلاثة لعملية التنازل عن الذات بشكل منفصل.
إذا كانت قيمة Variable Inc 10 في وقت معين ، فإن مؤشر الترابط 1 يقوم بعملية التمييز الذاتي على المتغير ، ويقرأ مؤشر الترابط 1 أولاً القيمة الأصلية لـ Variable Inc ، ثم يتم حظر مؤشر الترابط 1 ؛ ثم يقوم مؤشر الترابط 2 بتنفيذ عملية التمييز الذاتي على المتغير ، ويقرأ مؤشر الترابط 2 أيضًا القيمة الأصلية لـ Variable Inc. نظرًا لأن مؤشر الترابط 1 يقوم فقط بإجراء عملية قراءة على متغير Inc ولا يقوم بتعديل المتغير ، فلن يتسبب في أن خط ذاكرة التخزين المؤقت المتغير المتغير في مؤشر الترابط 2 ليكون غير صالح في الذاكرة العاملة ، لذلك سيذهب مؤشر الترابط 2 مباشرة إلى الذاكرة الرئيسية لقراءة قيمة Inc. عندما تبين أن قيمة Inc هي 10 ، ثم تؤدي زيادة قدرها 1 ، وتكتب 11 للذاكرة العاملة ، وأخيراً تكتبها إلى الذاكرة الرئيسية. ثم الموضوع 1 ثم ينفذ عملية الإضافة. منذ قراءة قيمة Inc ، لاحظ أن قيمة INC في الموضوع 1 لا تزال 10 في هذا الوقت ، لذلك بعد إضافة الموضوع 1 Inc ، تبلغ قيمة Inc 11 ، ثم تكتب 11 إلى ذاكرة العمل ، وأخيراً تكتبها إلى الذاكرة الرئيسية. ثم بعد أن تقوم الخيوطان بإجراء عملية تخصيص ذاتي ، تزيد شركة INC فقط بمقدار 1.
عملية التلقائية ليست عملية ذرية ، ولا يمكن أن تضمن المتقلبة أن تكون أي عملية على متغير ذري.
هل يمكن أن يضمن التقلب النظام؟
كما ذكرنا سابقًا ، يمكن للكلمة الرئيسية المتطايرة أن تحظر إعادة ترتيب التعليمات ، لذلك يمكن أن يضمن التقلب أمرًا إلى حد ما.
هناك معنيان ممنوعان من إعادة ترتيب الكلمات الرئيسية المتطايرة:
3. استخدم الكلمة الرئيسية المتطايرة بشكل صحيح
تمنع الكلمة الرئيسية المتزامنة مؤشرات ترابط متعددة من تنفيذ جزء من الكود في نفس الوقت ، مما سيؤثر بشكل كبير على كفاءة تنفيذ البرنامج. أداء الكلمة الرئيسية المتطايرة أفضل من مزامنة في بعض الحالات. ومع ذلك ، تجدر الإشارة إلى أن الكلمة الرئيسية المتطايرة لا يمكن أن تحل محل الكلمة الرئيسية المتزامنة ، لأن الكلمة الرئيسية المتطايرة لا يمكن أن تضمن ذرة العملية. بشكل عام ، يجب الوفاء بالشرطين التاليين عند استخدام متقلبة:
الشرط الأول هو أنه لا يمكن أن تكون عمليات مثل الاندماج الذاتي والانفصال عن النفس. كما ذكر أعلاه ، لا يضمن التقلب الذرة.
دعونا نعطي مثالا على هذا. يحتوي على ثابت: الحد الأدنى دائمًا أقل من أو يساوي الحد الأعلى.
رقم الفئة العامة {private folatile int lower ، apport ؛ public int getLower () {return lower ؛ } public int getupper () {return apper ؛ } public void setlower (int value) {if (value> apport) رمي alfortleargumentException (...) ؛ أقل = قيمة ؛ } public void setupper (int value) {if (value <lower) reming alficalaRgumentException (...) ؛ العلوي = القيمة ؛ }} وبهذه الطريقة ، يحد من متغيرات الحالة التي يتم تحديدها ، وبالتالي فإن تحديد الحقول السفلية والعليا لأن الأنواع المتطايرة لا تنفذ بالكامل سلامة مؤشر ترابط الفئة ، وبالتالي لا يزال يتطلب التزامن. خلاف ذلك ، إذا حدث تنفيذ مؤشر ترابط لتنفيذ setlower و setupper مع قيم غير متسقة في نفس الوقت ، فسيكون النطاق غير متسق. على سبيل المثال ، إذا كانت الحالة الأولية هي (0 ، 5) ، في نفس الوقت ، فإن سلسلة الرسائل A Setlower (4) و Prohet B تستدعي Setupper (3) ، فمن الواضح أن القيم المخزنة في المتقاطعين من خلال العمليتين لا تفي بالشروط ، فمن الواضح أن كلا الموضوعين سوف يمرران الشيكات للحماية ، بحيث تكون قيمة النطاق الأخيرة (4 ، 3) ، والتي من الواضح أنها من الواضح.
في الواقع ، هو ضمان ذرة العملية لاستخدام متقلبة. هناك سيناريوهان رئيسيان لاستخدام المتقلبة:
أعلام الحالة
الإغلاق المنطقي المتقلوب )
من المحتمل أن يتم استدعاء طريقة إيقاف التشغيل () من خارج الحلقة - أي في مؤشر ترابط آخر - لذلك يجب تنفيذ نوع من التزامن لضمان تنفيذ رؤية المتغير المكتوب بشكل صحيح. ومع ذلك ، فإن كتابة حلقة مع كتل متزامنة أكثر إزعاجًا من الكتابة بأعلام الحالة المتطايرة. نظرًا لأن المتقلبة تبسيط الترميز ولا تعتمد أعلام الحالة على أي حالة أخرى في البرنامج ، فهي مناسبة جدًا للمتقلب هنا.
وضع الفحص المزدوج (DCL)
الطبقة العامة Singleton {مثيل Singleton الثابت المتطرف الخاص = NULL ؛ public static singleton getInstance () {if (مثيل == null) {synchronized (this) {if (easty == null) {easty = new singleton () ؛ }} مثيل الإرجاع ؛ }} سيؤثر استخدام التقلب هنا بشكل أو بآخر على الأداء ، ولكن بالنظر إلى صحة البرنامج ، لا يزال من المفيد التضحية بهذا الأداء.
ميزة DCL هي أن لديها استخدام موارد عالية. يتم إنشاء كائنات Singleton فقط عندما يتم تنفيذ getInstance لأول مرة ، وهو فعال للغاية. العيب هو أن التفاعل أبطأ قليلاً عند التحميل لأول مرة ، وهناك عيوب معينة في بيئات التزامن العالية ، على الرغم من أن احتمال حدوثه صغير جدًا.
على الرغم من أن DCL يحل مشاكل استهلاك الموارد ، والمزامنة غير الضرورية ، وسلامة الخيط ، وما إلى ذلك ، إلى حد ما ، لا يزال يعاني من مشاكل في الفشل في بعض الحالات ، أي فشل DCL. في كتاب "ممارسة برمجة Java Concurrency" ، يوصي باستخدام الكود التالي (نمط Singleton static الداخلي) لتحل محل DCL:
الطبقة العامة Singleton {private Singleton () {} Singleton GetInstance () {return Singletonholder.sinstance ؛ } Singletonholder static static {private Static Final Sinstance = New Singleton () ؛ }} حول الشيكات المزدوجة ، يمكنك عرض
4. ملخص
بالمقارنة مع الأقفال ، فإن المتغير المتطرف بسيط للغاية ولكن في نفس الوقت آلية المزامنة الهشة للغاية التي ستوفر في بعض الحالات أداءً أفضل وقابلية التوسع من الأقفال. إذا اتبعت بدقة شروط استخدام المتقلبة التي تكون مستقلة حقًا عن المتغيرات الأخرى وقيمها السابقة ، فيمكنك استخدام متقلبة بدلاً من مزامنة لتبسيط الكود في بعض الحالات. ومع ذلك ، غالبًا ما يكون الكود باستخدام المتقلبة أكثر عرضة للأخطاء من التعليمات البرمجية باستخدام الأقفال. تقدم هذه المقالة حالتين للاستخدام الأكثر شيوعًا حيث يمكن استخدام متقلبة بدلاً من مزامنة. في حالات أخرى ، من الأفضل أن نستخدم المزامنة.
ما سبق هو كل شيء عن هذا المقال ، آمل أن يكون مفيدًا لتعلم الجميع.