قد يكون العديد من الأصدقاء قد سمعوا عن الكلمة الرئيسية المتطايرة وربما استخدموها. قبل Java 5 ، كانت كلمة رئيسية مثيرة للجدل ، حيث أن استخدامها في البرامج غالبًا ما أدى إلى نتائج غير متوقعة. فقط بعد أن استعادت Java 5 الكلمة الرئيسية المتطايرة حيويتها.
على الرغم من أن الكلمة الرئيسية المتطايرة سهلة الفهم حرفيًا ، إلا أنه ليس من السهل استخدامها جيدًا. نظرًا لأن الكلمة الرئيسية المتطايرة ترتبط بنموذج ذاكرة Java ، قبل إخبار المفتاح المتقلبة ، نفهم أولاً المفاهيم والمعرفة المتعلقة بنموذج الذاكرة ، ثم نحلل مبدأ التنفيذ للكلمة الرئيسية المتطايرة ، وأخيراً نعطي عدة سيناريوهات لاستخدام الكلمة الرئيسية المتطايرة.
هنا هو الخطوط العريضة لهذا المقال:
1. المفاهيم ذات الصلة لنماذج الذاكرة
كما نعلم جميعًا ، عندما يقوم الكمبيوتر بتنفيذ برنامج ما ، يتم تنفيذ كل تعليمات في وحدة المعالجة المركزية ، وخلال تنفيذ التعليمات ، سيتضمن حتما قراءة البيانات وكتابةها. نظرًا لأن البيانات المؤقتة أثناء تشغيل البرنامج يتم تخزينها في الذاكرة الرئيسية (الذاكرة الفعلية) ، فهناك مشكلة في هذا الوقت. نظرًا لأن سرعة تنفيذ وحدة المعالجة المركزية سريعة للغاية ، فإن عملية قراءة البيانات من الذاكرة وكتابة البيانات إلى الذاكرة أبطأ بكثير من تنفيذ التعليمات في وحدة المعالجة المركزية. لذلك ، إذا كان يجب تنفيذ عملية البيانات من خلال التفاعل مع الذاكرة في أي وقت ، فسيتم تقليل سرعة تنفيذ التعليمات إلى حد كبير. لذلك ، هناك ذاكرة التخزين المؤقت في وحدة المعالجة المركزية.
أي عند تشغيل البرنامج ، فإنه سيقوم بنسخ البيانات المطلوبة من قبل العملية من الذاكرة الرئيسية إلى ذاكرة التخزين المؤقت لوحدة المعالجة المركزية. ثم عندما تقوم وحدة المعالجة المركزية بإجراء العمليات الحسابية ، يمكنها قراءة البيانات مباشرة من ذاكرة التخزين المؤقت وكتابة البيانات إليها. بعد اكتمال العملية ، سيتم مسح البيانات الموجودة في ذاكرة التخزين المؤقت في الذاكرة الرئيسية. دعنا نعطي مثالًا بسيطًا ، مثل الرمز التالي:
i = i + 1 ؛
عندما يقوم مؤشر الترابط بتنفيذ هذا العبارة ، سيقرأ أولاً قيمة I من الذاكرة الرئيسية ، ثم نسخ نسخة إلى ذاكرة التخزين المؤقت ، ثم تقوم وحدة المعالجة المركزية بتنفيذ التعليمات لإضافة 1 إلى I ، ثم كتابة البيانات إلى ذاكرة التخزين المؤقت ، وأخيراً تحديث القيمة الأخيرة لـ I في ذاكرة التخزين المؤقت إلى الذاكرة الرئيسية.
لا توجد مشكلة في تشغيل هذا الرمز في سلسلة رسائل واحدة ، ولكن ستكون هناك مشاكل عند التشغيل في سلسلة متعددة. في وحدات المعالجة المركزية متعددة النواة ، قد يتم تشغيل كل مؤشر ترابط في وحدة المعالجة المركزية المختلفة ، لذلك كل مؤشر ترابط له ذاكرة التخزين المؤقت الخاصة به عند التشغيل (لاعتداء وحدات المعالجة المركزية أحادية النواة ، ستحدث هذه المشكلة بالفعل ، ولكن يتم تنفيذها بشكل منفصل في شكل جدولة مؤشرات الترابط). في هذه المقالة ، نأخذ وحدة المعالجة المركزية متعددة النواة كمثال.
على سبيل المثال ، يقوم اثنان بتنفيذ مؤشر ترابط هذا الرمز في نفس الوقت. إذا كانت قيمة I 0 في البداية ، فإننا نأمل أن تصبح قيمة I بعد تنفيذ الخيوط. ولكن هل سيكون هذا هو الحال؟
قد يكون هناك واحدة من المواقف التالية: في البداية ، يقرأ خيطان قيمة I وتخزينه في ذاكرة التخزين المؤقت الخاصة بوحدة المعالجة المركزية الخاصة بهما ، ثم يقوم مؤشر الترابط 1 بإجراء عملية لإضافة 1 ، ثم يكتب أحدث قيمة I إلى الذاكرة. في هذا الوقت ، لا تزال قيمة I في ذاكرة التخزين المؤقت للمعلومات 2 0. بعد إجراء عملية واحدة ، تكون قيمة I هي 1 ، ثم يكتب الموضوع 2 قيمة I إلى الذاكرة.
قيمة النتيجة النهائية I هي 1 ، وليس 2. هذه هي مشكلة تناسق ذاكرة التخزين المؤقت الشهيرة. عادة ما يطلق على هذا المتغير الذي يتم الوصول إليه بواسطة مؤشرات ترابط متعددة متغير مشترك.
بمعنى أنه إذا تم تخزين متغير في وحدة المعالجة المركزية المتعددة (عادة ما يحدث فقط أثناء البرمجة المتعددة) ، فقد تكون هناك مشكلة في عدم تناسق ذاكرة التخزين المؤقت.
من أجل حل مشكلة عدم تناسق ذاكرة التخزين المؤقت ، عادة ما يكون هناك حلان:
1) بإضافة قفل# قفل إلى الحافلة
2) من خلال بروتوكول تماسك ذاكرة التخزين المؤقت
يتم توفير هاتين الطريقتين على مستوى الأجهزة.
في وحدات المعالجة المركزية المبكرة ، تم حل مشكلة عدم تناسق ذاكرة التخزين المؤقت عن طريق إضافة قفل# أقفال إلى الحافلة. نظرًا لأن الاتصالات بين وحدة المعالجة المركزية والمكونات الأخرى يتم تنفيذها من خلال الحافلة ، إذا تمت إضافة الحافلة باستخدام قفل# قفل ، فهذا يعني أنه يتم حظر وحدات المعالجة المركزية الأخرى من الوصول إلى مكونات أخرى (مثل الذاكرة) ، بحيث يمكن لوحدة المعالجة المركزية واحدة فقط استخدام ذاكرة هذا المتغير. على سبيل المثال ، في المثال أعلاه ، إذا تم تنفيذ مؤشر ترابط I = I +1 ، وإذا تم إرسال إشارة LCOK# القفل على الحافلة أثناء تنفيذ هذا الرمز ، فحينئذٍ فقط بعد انتظار تنفيذ الكود بالكامل ، يمكن لوحدة المعالجة المركزية الأخرى قراءة المتغير من الذاكرة حيث يوجد المتغير الذي يقع عليه تحديد العمليات المقابلة. هذا يحل مشكلة عدم تناسق ذاكرة التخزين المؤقت.
لكن الطريقة أعلاه سيكون لها مشكلة ، لأن وحدات المعالجة المركزية الأخرى لا يمكنها الوصول إلى الذاكرة أثناء قفل الحافلة ، مما يؤدي إلى عدم الكفاءة.
لذلك يظهر بروتوكول تناسق ذاكرة التخزين المؤقت. الأكثر شهرة هو بروتوكول Intel's Mesi ، والذي يضمن أن تكون نسخة المتغيرات المشتركة المستخدمة في كل ذاكرة التخزين المؤقت متسقة. فكرتها الأساسية هي: عندما تكتب وحدة المعالجة المركزية البيانات ، إذا وجدت أن المتغير الذي يتم تشغيله هو متغير مشترك ، أي أن هناك نسخة من المتغير في وحدات المعالجة المركزية الأخرى ، فسيشير إلى وحدات المعالجة المركزية الأخرى لتعيين خط ذاكرة التخزين المؤقت للمتغير إلى حالة غير صالحة. لذلك ، عندما تحتاج وحدات المعالجة المركزية الأخرى إلى قراءة هذا المتغير وتجد أن خط ذاكرة التخزين المؤقت الذي يخبؤ المتغير في ذاكرة التخزين المؤقت الخاصة بهم غير صالح ، فإنه سيعيد قراءة الذاكرة.
2. ثلاثة مفاهيم في البرمجة المتزامنة
في البرمجة المتزامنة ، نواجه عادة المشكلات الثلاث التالية: مشكلة الذرة ، ومشكلة الرؤية ، ومشكلة منظمة. دعونا نلقي نظرة على هذه المفاهيم الثلاثة أولاً:
1
Atomicity: أي أن عملية واحدة أو عمليات متعددة إما يتم تنفيذها جميعًا ولن يتم مقاطعة عملية التنفيذ بأي عوامل ، أو لن يتم تنفيذها.
مثال كلاسيكي للغاية هو مشكلة تحويل الحساب المصرفي:
على سبيل المثال ، إذا قمت بنقل 1000 يوان من الحساب A إلى الحساب B ، فسيتضمن حتماً عمليتين: قم بطرح 1000 يوان من الحساب A وإضافة 1000 يوان إلى الحساب B.
فقط تخيل ما هي العواقب التي سيتسبب بها إذا لم تكن هاتان العمليتان ذرية. إذا تم طرح 1000 يوان من الحساب أ ، فسيتم إنهاء العملية فجأة. بعد ذلك ، تم سحب 500 يوان من B ، وبعد سحب 500 يوان ، ثم عملية إضافة 1000 يوان إلى الحساب B. سيؤدي ذلك إلى حقيقة أنه على الرغم من أن الحساب A يحتوي على 1000 يوان ، فإن الحساب B لم يتلق 1000 يوان.
لذلك ، يجب أن تكون هاتان العمليتان ذرية لضمان عدم وجود مشاكل غير متوقعة.
ما هي النتائج التي سوف تنعكس في البرمجة المتزامنة؟
لإعطاء أبسط مثال ، فكر في ما سيحدث إذا كانت عملية تعيين متغير 32 بت غير ذري؟
أنا = 9 ؛
إذا قام مؤشر ترابط بتنفيذ هذا البيان ، فسوف أفترض أن تعيين متغير 32 بت يتضمن عمليتين: تعيين أقل من 16 بت وتخصيص أعلى 16 بت.
ثم قد يحدث الموقف: عند كتابة القيمة المنخفضة 16 بت ، يتم مقاطعة فجأة ، وفي هذا الوقت ، يقرأ مؤشر ترابط آخر قيمة i ، ثم ما يقرأ هو البيانات الخاطئة.
2. الرؤية
تشير الرؤية إلى متى تصل مؤشرات الترابط المتعددة إلى نفس المتغير ، يقوم مؤشر ترابط واحد بتعديل قيمة المتغير ، ويمكن أن ترى مؤشرات الترابط الأخرى على الفور القيمة المعدلة.
للحصول على مثال بسيط ، راجع الكود التالي:
// الكود الذي تم تنفيذه بواسطة الموضوع 1 هو int i = 0 ؛ i = 10 ؛ // الكود الذي تم تنفيذه بواسطة الموضوع 2 هو j = i ؛
إذا كان مؤشر ترابط التنفيذ 1 هو وحدة المعالجة المركزية 1 وخيط التنفيذ 2 هو وحدة المعالجة المركزية 2. من التحليل أعلاه ، يمكننا أن نرى أنه عندما ينفذ الموضوع 1 الجملة I = 10 ، سيتم تحميل القيمة الأولية لـ I في ذاكرة التخزين المؤقت للوحدة المعالجة المركزية 1 ثم تعيين قيمة 10. ثم تصبح قيمة I في ذاكرة التخزين المؤقت CPU1 10 ، ولكن لا يتم كتابتها على الفور إلى الذاكرة الرئيسية.
في هذا الوقت ، يقوم الموضوع 2 بتنفيذ J = i ، وسيذهب أولاً إلى الذاكرة الرئيسية لقراءة قيمة I وتحميلها في ذاكرة التخزين المؤقت لـ CPU2. لاحظ أن قيمة I في الذاكرة لا تزال 0 ، وبالتالي فإن قيمة J ستكون 0 ، وليس 10.
هذه هي قضية الرؤية. بعد سلسلة 1 يعدل المتغير I ، لا يرى مؤشر الترابط 2 على الفور القيمة المعدلة بواسطة الموضوع 1.
3. الترتيب
الأمر: أي ، يتم تنفيذ ترتيب تنفيذ البرامج بترتيب الكود. للحصول على مثال بسيط ، راجع الكود التالي:
int i = 0 ؛ العلم المنطقي = خطأ ؛ i = 1 ؛ // بيان 1 العلم = صحيح ؛ // بيان 2
يحدد الكود أعلاه متغيرًا من النوع Int ، ومتغير من النوع المنطقي ، ثم يعين القيم إلى المتغيرين على التوالي. من منظور تسلسل الكود ، البيان 1 قبل البيان 2. لذلك عندما ينفذ JVM هذا الرمز بالفعل ، هل سيضمن تنفيذ البيان 1 قبل البيان 2؟ ليس بالضرورة ، لماذا؟ قد يحدث إعادة ترتيب التعليمات هنا.
دعنا نوضح ما هو إعادة ترتيب التعليمات. بشكل عام ، من أجل تحسين كفاءة تشغيل البرنامج ، يجوز للمعالج تحسين رمز الإدخال. لا يضمن أن ترتيب تنفيذ كل عبارة في البرنامج متسقة مع الطلب في الكود ، ولكنه سيضمن أن تكون نتيجة التنفيذ النهائية للبرنامج ونتيجة تسلسل تنفيذ الكود متسقة.
على سبيل المثال ، في الكود أعلاه ، الذي ينفذ البيان 1 والبيان 2 أولاً ، ليس له أي تأثير على نتيجة البرنامج النهائي ، فمن الممكن أنه خلال عملية التنفيذ ، يتم تنفيذ البيان 2 أولاً ويتم تنفيذ البيان 1 لاحقًا.
ولكن كن على دراية أنه على الرغم من أن المعالج سيعيد ترتيب التعليمات ، إلا أنه سيضمن أن تكون النتيجة النهائية للبرنامج هي نفس تسلسل تنفيذ الكود. إذن ما الذي يضمن ذلك؟ دعونا نلقي نظرة على المثال التالي:
int a = 10 ؛ // بيان 1int r = 2 ؛ // بيان 2A = A + 3 ؛ // بيان 3r = a*a ؛ // بيان 4
يحتوي هذا الرمز على 4 عبارات ، لذا فإن أمر التنفيذ المحتمل هو:
فهل من الممكن أن يكون أمر التنفيذ: البيان 2 البيان 1 البيان 4 البيان 3
هذا غير ممكن لأن المعالج سينظر في الاعتماد على البيانات بين التعليمات عند إعادة ترتيب. إذا كان يجب استخدام تعليمات التعليمات 2 نتيجة التعليمات 1 ، فسيضمن المعالج أن يتم تنفيذ التعليمات 1 قبل التعليمات 2.
على الرغم من أن إعادة الترتيب لن تؤثر على نتائج تنفيذ البرنامج داخل موضوع واحد ، فماذا عن Multithreading؟ لنرى مثالًا أدناه:
// الموضوع 1: context = loadContext () ؛ // state 1inited = true ؛ // state 2 // thread 2: بينما (! inited) {sleep ()} dosomethingWithConfig (context) ؛في الكود أعلاه ، نظرًا لأن البيانات 1 و 2 لا يوجد بها تبعيات للبيانات ، فقد يتم إعادة ترتيبها. في حالة حدوث إعادة ترتيب ، يتم تنفيذ العبارة 2 لأول مرة أثناء تنفيذ الموضوع 1 ، وسيعتقد هذا الموضوع 2 أن أعمال التهيئة قد تم الانتهاء منها ، وبعد ذلك سوف تقفز من الحلقة لتنفيذ طريقة DosomethingWithConfig (السياق). في هذا الوقت ، لا يتم تهيئة السياق ، مما سيؤدي إلى خطأ في البرنامج.
كما يتضح من ما سبق ، لن يؤثر إعادة ترتيب التعليمات على تنفيذ مؤشر ترابط واحد ، ولكنه سيؤثر على صحة التنفيذ المتزامن للمواضيع.
بمعنى آخر ، من أجل تنفيذ البرامج المتزامنة بشكل صحيح ، يجب ضمان الذرة والرؤية والنظام. طالما لم يكن أحدهما مضمونًا ، فقد يتسبب في تشغيل البرنامج بشكل غير صحيح.
3. جافا نموذج الذاكرة
تحدثت عن بعض المشكلات التي قد تنشأ في نماذج الذاكرة والبرمجة المتزامنة. دعنا نلقي نظرة على نموذج ذاكرة Java وندرس ما يضمن نموذج ذاكرة Java الذي يوفره لنا والأساليب والآليات المقدمة في Java لضمان صحة تنفيذ البرنامج عند إجراء البرمجة متعددة الخيوط.
في مواصفات الجهاز الظاهري Java ، تمت محاولة تحديد نموذج ذاكرة Java (JMM) لمنع اختلافات الوصول إلى الذاكرة بين منصات الأجهزة المختلفة وأنظمة التشغيل ، وذلك لتمكين برامج Java من تحقيق تأثيرات الوصول المتسقة للذاكرة على منصات مختلفة. إذن ما الذي ينص عليه نموذج ذاكرة جافا؟ يحدد قواعد الوصول للمتغيرات في البرنامج. لوضعه على نطاق أوسع ، فإنه يحدد ترتيب تنفيذ البرنامج. لاحظ أنه من أجل الحصول على أداء أفضل للتنفيذ ، لا يقيد نموذج ذاكرة Java محرك التنفيذ من استخدام سجلات المعالج أو ذاكرة التخزين المؤقت لتحسين سرعة تنفيذ التعليمات ، ولا يقيد المترجم لإعادة ترتيب التعليمات. بمعنى آخر ، في نموذج ذاكرة Java ، سيكون هناك أيضًا مشاكل في تناسق ذاكرة التخزين المؤقت وإعادة ترتيب التعليمات.
ينص طراز ذاكرة Java على أن جميع المتغيرات في الذاكرة الرئيسية (على غرار الذاكرة الفعلية المذكورة أعلاه) ، وكل مؤشر ترابط له ذاكرة العمل الخاصة به (على غرار ذاكرة التخزين المؤقت السابقة). يجب إجراء جميع عمليات مؤشر ترابط على متغير في الذاكرة العاملة ، ولا يمكن العمل مباشرة على الذاكرة الرئيسية. ولا يمكن لكل مؤشر ترابط الوصول إلى الذاكرة العاملة للمواضيع الأخرى.
لإعطاء مثال بسيط: في جافا ، قم بتنفيذ البيان التالي:
أنا = 10 ؛
يجب أن يقوم مؤشر ترابط التنفيذ أولاً بتعيين خط ذاكرة التخزين المؤقت حيث يوجد المتغير I في مؤشر ترابط العمل الخاص به ، ثم كتابته إلى الذاكرة الرئيسية. بدلاً من كتابة القيمة 10 مباشرة في الذاكرة الرئيسية.
فما هي الضمانات التي توفرها لغة جافا نفسها للذرية والرؤية والنظام؟
1
في Java ، فإن عمليات القراءة والتعيين لمتغيرات أنواع البيانات الأساسية هي العمليات الذرية ، أي أنه لا يمكن مقاطعة هذه العمليات وتنفيذها أو لا.
على الرغم من أن الجملة أعلاه تبدو بسيطة ، إلا أنها ليست سهلة الفهم. انظر المثال التالي I:
يرجى تحليل أي من العمليات التالية هي العمليات الذرية:
x = 10 ؛ // بيان 1y = x ؛ // بيان 2x ++ ؛ // بيان 3x = x + 1 ؛ // بيان 4
للوهلة الأولى ، قد يقول بعض الأصدقاء أن العمليات في العبارات الأربعة المذكورة أعلاه هي جميع العمليات الذرية. في الواقع ، البيان 1 فقط هو عملية ذرية ، ولا توجد أي من العبارات الثلاثة الأخرى هي العمليات الذرية.
يعين البيانات 1 مباشرة القيمة 10 إلى x ، مما يعني أن مؤشر الترابط ينفذ هذا البيان ويكتب القيمة 10 مباشرة في الذاكرة العاملة.
البيان 2 يحتوي في الواقع على 2 عملية. يحتاج أولاً إلى قراءة قيمة x ، ثم كتابة قيمة x إلى الذاكرة العاملة. على الرغم من أن العمليتين لقراءة قيمة X وكتابة قيمة X للذاكرة العاملة هما العمليات الذرية ، إلا أنهما ليسا عمليات ذرية معًا.
وبالمثل ، يتضمن X ++ و X = X+1 3 عمليات: اقرأ قيمة X ، وتنفيذ عملية إضافة 1 ، وكتابة القيمة الجديدة.
لذلك ، فقط تشغيل البيان 1 في العبارات الأربعة أعلاه هو ذري.
وبعبارة أخرى ، فإن القراءة والتعيين البسيطة فقط (ويجب تعيين الرقم لمتغير ، والتعيين المتبادل بين المتغيرات ليس عملية ذرية) هي عملية ذرية.
ومع ذلك ، هناك شيء واحد يجب ملاحظته هنا: بموجب النظام الأساسي 32 بت ، يجب إكمال قراءة وتعيين البيانات 64 بت من خلال عمليتين ، ولا يمكن ضمان ذريته. ومع ذلك ، يبدو أنه في أحدث JDK ، ضمنت JVM أن قراءة وتعيين بيانات 64 بت هي أيضًا تشغيل ذري.
مما سبق ، يمكن ملاحظة أن نموذج ذاكرة Java يضمن فقط أن القراءات والواجبات الأساسية هي عمليات ذرية. إذا كنت ترغب في تحقيق ذرة مجموعة أكبر من العمليات ، فيمكن تحقيق ذلك من خلال متزامن وقفل. نظرًا لأن المزامنة والقفل يمكن أن يضمن أن مؤشر ترابط واحد فقط ينفذ كتلة الكود في أي وقت ، فلن تكون هناك مشكلة في الذرة بشكل طبيعي ، وبالتالي ضمان الذرة.
2. الرؤية
للرؤية ، توفر Java الكلمة الرئيسية المتطايرة لضمان الرؤية.
عندما يتم تعديل متغير مشترك بواسطة متقلبة ، فإنه يضمن تحديث القيمة المعدلة إلى الذاكرة الرئيسية على الفور ، وعندما تحتاج مؤشرات الترابط الأخرى إلى قراءتها ، ستقرأ القيمة الجديدة في الذاكرة.
ومع ذلك ، لا يمكن للمتغيرات المشتركة العادية ضمان الرؤية ، لأنه غير مؤكد عندما يتم كتابة المتغير المشترك العادي إلى الذاكرة الرئيسية بعد تعديله. عندما تقرأها مؤشرات الترابط الأخرى ، قد لا تزال القيمة القديمة الأصلية في الذاكرة ، لذلك لا يمكن ضمان الرؤية.
بالإضافة إلى ذلك ، يمكن أن يضمن المزامنة والقفل أيضًا الرؤية. يمكن أن يضمن المزامنة والقفل أن مؤشر ترابط واحد فقط يكتسب القفل في نفس الوقت ويقوم بتنفيذ رمز التزامن. قبل إطلاق القفل ، سيتم تحديث تعديل المتغير إلى الذاكرة الرئيسية. لذلك ، يمكن ضمان الرؤية.
3. الترتيب
في نموذج ذاكرة Java ، يُسمح للمترجمين والمعالجات بإعادة ترتيب التعليمات ، لكن عملية إعادة الترتيب لن تؤثر على تنفيذ البرامج المفردة ، ولكنها ستؤثر على صحة التنفيذ المتزامن متعدد الخيوط.
في Java ، يمكن ضمان "خط ترتيب" معين من خلال الكلمة الرئيسية المتقلبة (يتم شرح المبدأ المحدد في القسم التالي). بالإضافة إلى ذلك ، يمكن استخدام متزامن وقفل لضمان الطلب. من الواضح أن المزامنة والقفل تضمن أن هناك مؤشر ترابط ينفذ رمز التزامن في كل لحظة ، وهو ما يعادل ترك مؤشرات الترابط تنفيذ رمز التزامن بالتسلسل ، مما يضمن الطلب بشكل طبيعي.
بالإضافة إلى ذلك ، يحتوي نموذج ذاكرة Java على بعض "Orderline" الفطري ، أي أنه يمكن ضمانه دون أي وسيلة ، والتي عادة ما تسمى مبدأ SECT-Be قبل. إذا كان لا يمكن اشتقاق ترتيب التنفيذ لعمليتين من مبدأ SECT-Be-Be-Frear ، فلا يمكنهم ضمان نظامهم ويمكن أن يعيد ترتيب الأجهزة الافتراضية حسب الرغبة.
دعونا نقدم مبدأ الحدوث (مبدأ الحدوث الأولوية):
هذه المبادئ الثمانية مثبتة من "الفهم المتعمق للأجهزة الافتراضية Java".
من بين هذه القواعد الثمانية ، تكون القواعد الأربع الأولى أكثر أهمية ، في حين أن القواعد الأربعة الأخيرة كلها واضحة.
دعنا نوضح القواعد الأربع الأولى أدناه:
بالنسبة لقواعد طلب البرنامج ، أفهم أن تنفيذ رمز البرنامج يبدو أنه يتم طلبه في موضوع واحد. لاحظ أنه على الرغم من أن هذه القاعدة تذكر أن "العملية المكتوبة في المقدمة تحدث أولاً في العملية المكتوبة في الخلف" ، يجب أن يكون هذا هو الترتيب الذي يبدو أن البرنامج يتم تنفيذه في تسلسل الكود ، لأن الجهاز الظاهري قد يعيد ترتيب رمز البرنامج. على الرغم من إجراء إعادة الترتيب ، فإن نتيجة التنفيذ النهائية تتوافق مع تنفيذ تسلسل البرنامج ، وسيقوم فقط بإعادة ترتيب الإرشادات التي لا تحتوي على تبعيات للبيانات. لذلك ، في مؤشر ترابط واحد ، يبدو أن تنفيذ البرنامج يتم تنفيذه بطريقة منظمة ، والتي يجب فهمها بعناية. في الواقع ، يتم استخدام هذه القاعدة لضمان صحة نتائج تنفيذ البرنامج في مؤشر ترابط واحد ، ولكن لا يمكن أن تضمن صحة البرنامج بطريقة متعددة الخيوط.
القاعدة الثانية أسهل أيضًا فهمها ، أي إذا كان القفل نفسه في حالة مغلقة ، فيجب إصداره قبل استمرار عملية القفل.
القاعدة الثالثة هي قاعدة مهمة نسبيًا وهي أيضًا ما سيتم مناقشته لاحقًا. بشكل حدسي ، إذا كتب مؤشر ترابط متغير أولاً ثم يقرأ مؤشر ترابط ، فسيحدث عملية الكتابة بالتأكيد أولاً في عملية القراءة.
تعكس القاعدة الرابعة في الواقع أن مبدأ الحدوث قبل.
4. تحليل متعمق للكلمات الرئيسية المتطايرة
لقد تحدثت عن الكثير من الأشياء من قبل ، لكنها في الواقع تمهد الطريق لإخبار الكلمة الرئيسية المتقلبة ، لذلك دعونا نصل إلى الموضوع.
1. دلالات ثنائية الطبقة من الكلمات الرئيسية المتطايرة
بمجرد تعديل متغير مشترك (متغيرات عضو الفئة ، متغيرات الأعضاء الثابتة) ، فإنه يحتوي على طبقتين من الدلالات:
1) ضمان رؤية مؤشرات ترابط مختلفة عند تشغيل هذا المتغير ، أي ، يقوم مؤشر ترابط واحد بتعديل قيمة متغير معين ، وتكون هذه القيمة الجديدة مرئية على الفور إلى مؤشرات الترابط الأخرى.
2) يحظر إعادة ترتيب التعليمات.
دعونا نلقي نظرة على قطعة من الكود أولاً. إذا تم تنفيذ الموضوع 1 أولاً وتم تنفيذ الموضوع 2 لاحقًا:
// thread 1Boolean stop = false ؛ بينما (! توقف) {dosomething () ؛} // thread 2stop = true ؛هذا الرمز عبارة عن جزء نموذجي للغاية من الكود ، وقد يستخدم الكثير من الأشخاص طريقة الترميز هذه عند مقاطعة الخيوط. ولكن في الواقع ، هل سيعمل هذا الرمز بشكل صحيح تمامًا؟ هل ستتم مقاطعة الموضوع؟ ليس بالضرورة. ولعل في معظم الوقت ، يمكن لهذا الرمز مقاطعة مؤشرات الترابط ، ولكنه قد يتسبب أيضًا في عدم توقف مؤشر الترابط (على الرغم من أن هذا الاحتمال صغير جدًا ، بمجرد حدوث ذلك ، فإنه سيؤدي إلى حلقة ميتة).
دعونا نوضح سبب فشل هذا الرمز في انقطاع مؤشر الترابط. كما هو موضح سابقًا ، يحتوي كل مؤشر ترابط على ذاكرة العمل الخاصة به أثناء التشغيل ، لذلك عند تشغيل الموضوع 1 ، فإنه سينسخ قيمة متغير STOP ويضعه في ذاكرته العاملة.
ثم عندما يغير الموضوع 2 قيمة متغير STOP ، ولكن لم يكن لديه وقت لكتابتها إلى الذاكرة الرئيسية ، فإن الموضوع 2 يذهب للقيام بأشياء أخرى ، ثم لا يعرف الموضوع 1 عن تغييرات الموضوع 2 إلى متغير الإيقاف ، لذلك سيستمر في الحلقة.
ولكن بعد التعديل مع متقلبة يصبح الأمر مختلفًا:
أولاً: سيؤدي استخدام الكلمة الرئيسية المتطايرة إلى إجبار القيمة المعدلة التي سيتم كتابتها إلى الذاكرة الرئيسية على الفور ؛
ثانياً: إذا كنت تستخدم الكلمة الرئيسية المتطايرة ، عندما يقوم مؤشر الترابط 2 بتعديلها ، فسيكون خط ذاكرة التخزين المؤقت لمتغير ذاكرة التخزين المؤقت في ذاكرة العمل 1 غير صالح (إذا كان ينعكس في طبقة الأجهزة ، فإن خط ذاكرة التخزين المؤقت المقابلة في ذاكرة التخزين المؤقت L1 أو L2 من وحدة المعالجة المركزية غير صالحة) ؛
ثالثًا: نظرًا لأن خط ذاكرة التخزين المؤقت الخاصة بمتغير ذاكرة التخزين المؤقت في ذاكرة العمل في مؤشر الترابط 1 غير صالح ، فإن مؤشر الترابط 1 سيقرأه في الذاكرة الرئيسية عندما يقرأ قيمة التوقف المتغير مرة أخرى.
ثم عندما يقوم مؤشر الترابط 2 بتعديل قيمة الإيقاف (بالطبع ، هناك عمليتان هنا ، وتعديل القيمة في ذاكرة العمل في مؤشر الترابط 2 ، ثم كتابة القيمة المعدلة للذاكرة) ، سيكون خط ذاكرة التخزين المؤقت لوقف ذاكرة التخزين المؤقت في ذاكرة العمل 1 غير صالح. عندما يقرأ الموضوع 1 ، يجد أن خط ذاكرة التخزين المؤقت غير صالح. سوف ينتظر تحديث عنوان الذاكرة الرئيسي المقابل لخط ذاكرة التخزين المؤقت ، ثم قراءة أحدث قيمة في الذاكرة الرئيسية المقابلة.
ثم ما يقرأ مؤشر الترابط 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. ولكن في الواقع ، سيجد تشغيله أن نتائج كل تشغيل غير متسقة ، وهي أقل من 10،000.
قد يكون لدى بعض الأصدقاء أسئلة ، إنه خطأ. ما ورد أعلاه هو إجراء عملية التمييز الذاتي على المتغير. نظرًا لأن المتقلبة يضمن الرؤية ، بعد التمييز الذاتي لـ INC في كل مؤشر ترابط ، يمكن رؤية القيمة المعدلة في مؤشرات الترابط الأخرى. لذلك ، أجرت 10 مؤشرات ترابط 1000 عملية على التوالي ، وبالتالي يجب أن تكون القيمة النهائية لـ INC 1000*10 = 10000.
هناك سوء فهم هنا. يمكن للكلمة الرئيسية المتطايرة أن تضمن الرؤية ، لكن البرنامج أعلاه خاطئ لأنه لا يمكن أن يضمن الذرة. لا يمكن أن تضمن الرؤية إلا أن القيمة الأخيرة تتم قراءة في كل مرة ، ولكن لا يمكن أن تضمن تقلبات تشغيل متغيرات المتغيرات.
كما ذكرنا سابقًا ، فإن عملية التكرار التلقائي ليست ذرية. ويشمل قراءة القيمة الأصلية للمتغير ، وإجراء عملية إضافية ، والكتابة إلى الذاكرة العاملة. بمعنى أنه قد يتم تنفيذ العمليات الفرعية الثلاثة لعملية التنظيم الذاتي بشكل منفصل ، مما قد يؤدي إلى الموقف التالي:
إذا كانت قيمة المتغير في وقت معين هي 10 ،
الموضوع 1 يؤدي عملية التمييز الذاتي على المتغير. يقرأ الموضوع 1 أولاً القيمة الأصلية لـ Variable Inc ، ثم يتم حظر الموضوع 1 ؛
ثم يقوم مؤشر الترابط 2 بتنفيذ عملية التمييز الذاتي على المتغير ، ويقرأ مؤشر الترابط 2 أيضًا القيمة الأصلية لـ Variable Inc. نظرًا لأن مؤشر الترابط 1 يقوم فقط بإجراء عملية قراءة على متغير Inc ولا يعدل المتغير ، فلن يتسبب في أن خط ذاكرة التخزين المؤقت لمتغير Cache Inc Inc في مؤشر الترابط 2 ليكون غير صالح. لذلك ، سيذهب الموضوع 2 مباشرة إلى الذاكرة الرئيسية لقراءة قيمة INC. عندما يتم العثور على أن قيمة شركة INC هي 10 ، ثم تؤدي عملية لإضافة 1 ، ويكتب 11 إلى الذاكرة العاملة ، وأخيراً تكتبها إلى الذاكرة الرئيسية.
ثم الموضوع 1 ثم ينفذ عملية الإضافة. منذ قراءة قيمة Inc ، لاحظ أن قيمة INC في الموضوع 1 لا تزال 10 في هذا الوقت ، لذلك بعد إضافة الموضوع 1 Inc ، تبلغ قيمة Inc 11 ، ثم تكتب 11 إلى ذاكرة العمل ، وأخيراً تكتبها إلى الذاكرة الرئيسية.
ثم بعد أن تقوم الخيوطان بإجراء عملية تخصيص ذاتي ، تزيد شركة INC فقط بمقدار 1.
بعد شرح ذلك ، قد يكون لدى بعض الأصدقاء أسئلة ، فهذا خطأ. أليس من المضمون أن يقوم المتغير بإبطال خط ذاكرة التخزين المؤقت عند تعديل المتغير المتطاير؟ ثم سوف تقرأ المواضيع الأخرى القيمة الجديدة. نعم ، هذا صحيح. هذه هي القاعدة المتغيرة المتقلبة في قاعدة SECT-Beoreft أعلاه ، ولكن تجدر الإشارة إلى أنه إذا قرأ الموضوع 1 المتغير وتم حظره ، فلن يتم تعديل قيمة INC. بعد ذلك ، على الرغم من أن المتقلبة يمكن أن يضمن أن مؤشر الترابط 2 يقرأ قيمة المتغير المؤدي من الذاكرة ، إلا أن مؤشر الترابط 1 لم يعدله ، لذلك لن يرى مؤشر الترابط 2 القيمة المعدلة على الإطلاق.
السبب الجذري هو أن عملية التآكل التلقائي ليست عملية ذرية ، ولا يمكن أن يضمن التقلب أن أي عملية على المتغيرات ذرية.
تغيير الرمز أعلاه إلى أي مما يلي يمكن أن يحقق التأثير:
استخدم المزامنة:
اختبار الطبقة العامة {public 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) ؛ }} باستخدام القفل:
اختبار الطبقة العامة {public int inc = 0 ؛ قفل قفل = جديد reentrantlock () ؛ زيادة الفراغ العام () {lock.lock () ؛ حاول {inc ++ ؛ } أخيرًا {lock.unlock () ؛ }} 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) ؛ }} باستخدام AtomicInteger:
اختبار الطبقة العامة {public AtomicInteger Inc = new AtomicInteger () ؛ زيادة الفراغ العام () {inc.getandIncrement () ؛ } 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) ؛ }}يتم توفير بعض فئات التشغيل الذرية ضمن java.util.util.concurrent.Atomic Package of Java 1.5 ، أي المخلصة الذاتية (إضافة عملية واحدة) ، والانفصال الذاتي (إضافة عملية) ، وتشغيل الإضافة (إضافة رقم) ، وتشغيل الطرح (إضافة رقم) من أنواع البيانات الأساسية لضمان أن تكون هذه العمليات في العمليات. يستخدم Atomic CAS لتنفيذ العمليات الذرية (مقارنة ومبادلة). يتم تنفيذ CAS فعليًا باستخدام تعليمات CMPXCHG التي يوفرها المعالج ، ويقوم المعالج بتنفيذ تعليمات CMPXCHG هي عملية ذرية.
3. هل من المتقلبة ضمان النظام؟
كما ذكرنا سابقًا ، يمكن للكلمة الرئيسية المتطايرة أن تحظر إعادة ترتيب التعليمات ، لذلك يمكن أن يضمن التقلب أمرًا إلى حد ما.
هناك معنيان ممنوعان من إعادة ترتيب الكلمات الرئيسية المتطايرة:
1) عندما ينفذ البرنامج عملية قراءة أو كتابة للمتغير المتقلبة ، يجب إجراء جميع التغييرات على العمليات السابقة ، والنتيجة مرئية بالفعل للعمليات اللاحقة ؛ يجب عدم إجراء العمليات اللاحقة بعد ؛
2) عند إجراء تحسين التعليمات ، لا يمكن وضع العبارة التي تم الوصول إليها إلى المتغير المتطاير خلفه ، ولا يمكن وضع العبارات التالية للمتغير المتطاير قبله.
ربما ما يقال أعلاه هو مربك بعض الشيء ، لذلك أعط مثالا بسيطا:
// x و y متغيرات غير متطايرة // العلم متقلبة متغير x = 2 ؛ // بيان 1y = 0 ؛ // بيان 2flag = صحيح ؛ // بيان 3x = 4 ؛ // بيان 4y = -1 ؛ // بيان 5
نظرًا لأن متغير العلامة هو متغير متقلبة ، عند إجراء عملية إعادة ترتيب التعليمات ، لن يتم وضع البيان 3 قبل البيان 1 و 2 ، ولن يتم وضعه بعد البيان 3 ، والبيان 4 و 5. ومع ذلك ، لا يضمن ضمان ترتيب البيان 1 والبيان 2 وترتيب البيان 4 والبيان 5.
علاوة على ذلك ، يمكن للكلمة الرئيسية المتقلبة التأكد من أنه عند تنفيذ البيان 3 ، يجب تنفيذ البيان 1 والبيان 2 ، ونتائج تنفيذ البيان 1 والبيان 2 مرئية للبيان 3 ، البيان 4 ، والبيان 5.
لذلك دعنا نعود إلى المثال السابق:
// الموضوع 1: context = loadContext () ؛ // state 1inited = true ؛ // state 2 // thread 2: بينما (! inited) {sleep ()} dosomethingWithConfig (context) ؛عندما أعطيت هذا المثال ، ذكرت أنه من الممكن أن يتم تنفيذ البيان 2 قبل البيان 1 ، قد يتسبب في عدم تهيئة السياق ، ويستخدم الموضوع 2 السياق غير الضروري للعمل ، مما يؤدي إلى خطأ في البرنامج.
إذا تم تعديل المتغير المعتمد باستخدام الكلمة الرئيسية المتطايرة ، فلن تحدث هذه المشكلة ، لأنه عند تنفيذ البيان 2 ، سيضمن بالتأكيد تهيئة السياق.
4. مبدأ وتنفيذ الآلية المتقلبة
الوصف السابق لبعض استخدامات الكلمة الرئيسية المتطايرة التي نشأت منها. دعونا نناقش كيف يضمن التقلب الرؤية ويحظر التعليمات التي تم إعادة ترتيبها.
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
五.使用volatile关键字的场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
1.状态标记量
volatile boolean flag = false; while(!flag){ doSomething();} public void setFlag() { flag = true;} volatile boolean inited = false;//线程1:context = loadContext(); inited = true; //线程2:while(!inited ){sleep()}doSomethingwithconfig(context);2.double check
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); }} مثيل الإرجاع ؛ }}مراجع:
《Java编程思想》
《深入理解Java虚拟机》
The above is all the content of this article. آمل أن يكون ذلك مفيدًا لتعلم الجميع وآمل أن يدعم الجميع wulin.com أكثر.