المحتوى الرئيسي لهذه المقالة هو نقطة معرفة مشتركة في مقابلة Java: الكلمات الرئيسية المتقلبة. تقدم هذه المقالة جميع جوانب الكلمات الرئيسية المتطايرة بالتفصيل. آمل أنه بعد قراءة هذا المقال ، يمكنك حل المشكلات ذات الصلة بالكلمات الرئيسية المتقلبة تمامًا.
في المقابلات الوظيفية المتعلقة بـ Java ، يرغب العديد من المقابلات في دراسة فهم القائم بإجراء المقابلة للتزامن Java. باستخدام الكلمة الرئيسية المتطايرة كنقطة دخول صغيرة ، يمكنك في كثير من الأحيان أن تسأل طراز ذاكرة Java (JMM) وبعض ميزات البرمجة المتزامنة Java. بتعمق ، يمكنك أيضًا فحص المعرفة المتعلقة بتنفيذ نظام التشغيل JVM والتشغيل. دعونا نأخذ عملية مقابلة افتراضية لاكتساب فهم متعمق للكلمة الرئيسية Volitile!
بقدر ما أفهم ، فإن المتغيرات المشتركة التي تم تعديلها بواسطة المتقلبة لها خصائصان التاليةان:
1. ضمان رؤية ذاكرة مؤشرات الترابط المختلفة للعملية المتغيرة ؛
2. حظر إعادة ترتيب الأمر
هذا كثير للحديث عنه ، لذلك سأبدأ بنموذج ذاكرة جافا. تحاول مواصفات الجهاز الظاهري Java تحديد نموذج ذاكرة Java (JMM) لحظر اختلافات الوصول إلى الذاكرة بين مختلف الأجهزة وأنظمة التشغيل ، بحيث يمكن لبرامج Java تحقيق تأثيرات متسقة للوصول إلى الذاكرة على منصات مختلفة. ببساطة ، نظرًا لأن وحدة المعالجة المركزية تنفذ التعليمات بسرعة كبيرة ، فإن سرعة الوصول إلى الذاكرة أبطأ بكثير ، والفرق ليس ترتيبًا كبيرًا ، فقد أضاف اللاعبين الكبار الذين يعملون على المعالج عدة طبقات من ذاكرة التخزين المؤقت إلى وحدة المعالجة المركزية. في نموذج ذاكرة Java ، يتم استخلاص التحسين أعلاه مرة أخرى. ينص JMM على أن جميع المتغيرات موجودة في الذاكرة الرئيسية ، على غرار الذاكرة العادية المذكورة أعلاه ، وكل مؤشر ترابط يحتوي على ذاكرة العمل الخاصة به. يمكن اعتباره سجلًا أو ذاكرة التخزين المؤقت على وحدة المعالجة المركزية لسهولة الفهم. لذلك ، تعتمد عمليات الخيوط بشكل أساسي على ذاكرة العمل. يمكنهم فقط الوصول إلى ذاكرتهم العاملة ، ويجب عليهم مزامنة القيمة إلى الذاكرة الرئيسية قبل وبعد العمل. لا أعرف حتى ما قلته ، خذ قطعة من الورق لرسم:
عند تنفيذ مؤشر ترابط ، سيتم قراءة قيمة المتغير أولاً من الذاكرة الرئيسية ، ثم يتم تحميلها على النسخة في الذاكرة العاملة ، ثم تم نقلها إلى المعالج للتنفيذ. بعد اكتمال التنفيذ ، سيتم تعيين قيمة في الذاكرة العاملة ، ثم يتم نقل القيمة في الذاكرة العاملة إلى الذاكرة الرئيسية ، وسيتم تحديث القيمة في الذاكرة الرئيسية. على الرغم من أن استخدام الذاكرة العاملة والذاكرة الرئيسية أسرع ، إلا أنه يجلب أيضًا بعض المشكلات. على سبيل المثال ، انظر إلى المثال التالي:
i = i + 1 ؛
على افتراض أن القيمة الأولية لـ I هي 0 ، عندما ينفذها مؤشر ترابط واحد فقط ، ستحصل النتيجة بالتأكيد على 1. عندما يتم تنفيذ موضوعين ، هل ستحصل النتيجة على 2؟ هذا ليس هو الحال بالضرورة. قد يكون هذا هو الحال:
الموضوع 1: تحميل i من الذاكرة الرئيسية // i = 0 i + 1 // i = 1 موضوع 2: تحميل i من الذاكرة الرئيسية // لأن الموضوع 1 لم يكتب قيمة I مرة أخرى إلى الذاكرة الرئيسية ، ما زلت 0 i + 1 // i = 1 thread 1: حفظ i إلى مؤشر ترابط الذاكرة الرئيسي 2: حفظ i إلى الذاكرة الرئيسية
إذا اتبع ترابطان عملية التنفيذ أعلاه ، فإن القيمة الأخيرة لـ I هي في الواقع 1. إذا كانت الكتابة الأخيرة بطيئة ، ويمكنك قراءة قيمة I مرة أخرى ، فقد تكون 0 ، وهي مشكلة عدم تناسق ذاكرة التخزين المؤقت. فيما يلي ذكر السؤال الذي طرحته للتو. تم تأسيس JMM بشكل أساسي حول كيفية التعامل مع الخصائص الثلاث للذرية والرؤية والنظام في عملية التزامن. من خلال حل هذه المشكلات الثلاث ، يمكن حل مشكلة عدم تناسق ذاكرة التخزين المؤقت. ومتقلب يرتبط بالرؤية والنظام.
1. Atomicity: في Java ، فإن عمليات القراءة والتعيين لأنواع البيانات الأساسية هي العمليات الذرية. تعني العمليات الذرية المزعومة أن هذه العمليات غير متقطعة ويجب إكمالها لفترة معينة من الزمن ، أو لن يتم تنفيذها. على سبيل المثال:
i = 2 ؛ j = i ؛ i ++ ؛ i = i+1 ؛
من بين العمليات الأربع المذكورة أعلاه ، I = 2 هي عملية قراءة ، والتي يجب أن تكون عملية ذرية. J = أعتقد أنها عملية ذرية. في الواقع ، ينقسم إلى خطوتين. واحد هو قراءة قيمة i ، ثم تعيين القيمة إلى j. هذه عملية من خطوتين. لا يمكن أن يطلق عليه عملية ذرية. i ++ و i = i+ 1 متكافئة بالفعل. اقرأ قيمة i ، أضف 1 ، واكتبها مرة أخرى إلى الذاكرة الرئيسية. هذه هي عملية من 3 خطوات. لذلك ، في المثال أعلاه ، قد يكون للقيمة الأخيرة العديد من المواقف لأنها لا يمكن أن تلبي الذرة. وبهذه الطريقة ، لا يوجد سوى قراءة بسيطة. المهمة هي عملية ذرية ، أو مجرد مهمة عددية. إذا كنت تستخدم المتغيرات ، فهناك عملية إضافية لقراءة القيمة المتغيرة. الاستثناء هو أن مواصفات الجهاز الظاهري تتيح معالجة أنواع البيانات 64 بت (طويلة ومزدوجة) في عمليتين ، لكن أحدث تطبيق JDK لا يزال ينفذ العمليات الذرية. JMM فقط ينفذ الذرة الأساسية. يجب مزامنة العمليات مثل i ++ أعلاه وقفل لضمان ذرة الكود بأكمله. قبل أن يطلق الخيط القفل ، سوف يقوم حتماً بتنظيف قيمة أنا أعود إلى الذاكرة الرئيسية. 2. الرؤية: الحديث عن الرؤية ، تستخدم Java متقلبة لتوفير الرؤية. عندما يتم تعديل المتغير بواسطة متقلبة ، سيتم تحديث التعديل إليه على الفور إلى الذاكرة الرئيسية. عندما تحتاج مؤشرات الترابط الأخرى إلى قراءة المتغير ، سيتم قراءة القيمة الجديدة في الذاكرة. هذا غير مضمون من قبل المتغيرات العادية. في الواقع ، يمكن أن يضمن المزامنة والقفل أيضًا الرؤية. قبل أن يصدر مؤشر الترابط القفل ، سيقوم بمسح جميع القيم المتغيرة المشتركة مرة أخرى إلى الذاكرة الرئيسية ، ولكن متزامنة وقفل أكثر تكلفة. 3. طلب JMM يسمح للمترجم والمعالج بإعادة ترتيب التعليمات ، ولكنه ينص على الدلالات المؤهلة ، أي ، بغض النظر عن إعادة الترتيب ، لا يمكن تغيير نتيجة تنفيذ البرنامج. على سبيل المثال ، قطاع البرنامج التالي:
مزدوج PI = 3.14 ؛ // adouble r = 1 ؛ // bdouble s = pi * r * r ؛ // c
يمكن تنفيذ البيان أعلاه في A-> B-> C ، مع النتيجة هي 3.14 ، ولكن يمكن تنفيذها أيضًا بترتيب B-> A-> C. نظرًا لأن A و B هما بيانان مستقلان ، في حين أن C يعتمد على A و B ، يمكن إعادة ترتيب A و B ، ولكن لا يمكن تصنيف C أولاً في A و B. jmm لا يؤثر إعادة الترتيب على تنفيذ موضوع واحد ، ولكن المشكلات عرضة للحدوث في متعدد الخيوط. على سبيل المثال ، رمز مثل هذا:
int a = 0 ؛ bool flag = false ؛ public void write () {a = 2 ؛ // 1 العلم = صحيح ؛ // 2} void public multiply () {if (flag) {// 3 int ret = a * a ؛ // 4}}إذا قام ترابطان بتنفيذ قطاع الرمز أعلاه ، فإن مؤشر الترابط 1 ينفذ أولاً ، ثم يتم تنفيذ مؤشر الترابط 2 ، ثم ينفذ مضاعف ، هل يجب أن تكون قيمة RET 4؟ النتيجة ليست بالضرورة:
كما هو موضح في الشكل ، يتم إعادة ترتيب 1 و 2 في طريقة الكتابة. يقوم الموضوع 1 أولاً بتعيين العلامة إلى True ، ثم ينفذها إلى الموضوع 2 ، ويحسب Ret مباشرة النتيجة ، ثم إلى سلسلة 1. في هذا الوقت ، يتم تعيين A إلى 2 ، والتي من الواضح أنها خطوة واحدة في وقت لاحق. في هذا الوقت ، يمكنك إضافة الكلمة الرئيسية المتطايرة إلى العلم ، وحظر إعادة ترتيب ، والتي يمكن أن تضمن ترتيب البرنامج ، ويمكنك أيضًا استخدام ثقيل الوزن المتزامن والقفل لضمان النظام. يمكنهم التأكد من تنفيذ الرمز في هذا المجال في وقت واحد. بالإضافة إلى ذلك ، لدى JMM بعض الترتيب الفطري ، أي النظام الذي يمكن ضمانه دون أي وسيلة ، والتي عادة ما تسمى مبدأ SECT-Be قبل. << JSR-133: نموذج ذاكرة Java ومواصفات مؤشرات الترابط >> يحدد القواعد التالية: 1. قواعد تسلسل البرنامج: لكل عملية في مؤشر ترابط ، يتم استخدام Select-Beorefor المجال المتطاير 4. قابلة للعبور: إذا حدث قبل B و B من قبل C ، فعندئذ يحدث قبل C. من مؤشر ترابط يعود بنجاح من عملية threadb.join () في مؤشر الترابط A. 7. interrupt () مبدأ: تحدث طريقة استدعاء مؤشر ترابط () أولاً عند اكتشاف حدث المقاطعة بواسطة رمز مؤشر الترابط المقاطع. يمكنك استخدام Thread.Interrupted () طريقة للكشف عن ما إذا كان هناك انقطاع. 8. نهائيات () مبدأ: يحدث إكمال التهيئة للكائن أولاً عندما تبدأ طريقة النهائيات (). تشير القاعدة الأولى لقاعدة تسلسل البرنامج إلى أنه في مؤشر ترابط ، تكون جميع العمليات متتالية ، ولكن في JMM ، طالما أن نتيجة التنفيذ هي نفسها ، يُسمح بإعادة الترتيب. إن تركيز Sevent-be-forne هنا هو أيضًا صحة نتيجة التنفيذ ذات الخيوط الواحدة ، ولكن لا يمكن ضمان أن الأمر نفسه ينطبق على متعدد الخيوط. القاعدة 2 من السهل فهم قواعد الشاشة. قبل إضافة القفل ، يمكنك متابعة إضافة القفل فقط. تنطبق القاعدة 3 على المتقلبة المعنية. إذا كتب مؤشر ترابط أحد المتغير أولاً وقراءته مؤشر ترابط آخر ، فيجب أن تكون عملية الكتابة قبل عملية القراءة. القاعدة الرابعة هي عابرة الحدث قبل. لن أخوض في تفاصيل حول القلة التالية.
ثم نحتاج إلى إعادة إعادة تأمين القواعد المتغيرة المتقلبة: اكتب مجالًا متقلبة ، قبل أن يقرأ هذا المجال المتقلب لاحقًا. اسمحوا لي أن أخرج هذا مرة أخرى. في الواقع ، إذا تم الإعلان عن متغير متقلبة ، فعندما أقرأ المتغير ، يمكنني دائمًا قراءة أحدث قيمته. هنا ، تعني القيمة الأخيرة أنه بغض النظر عن مؤشر ترابط آخر يكتب المتغير ، سيتم تحديثه إلى الذاكرة الرئيسية على الفور. يمكنني أيضًا قراءة القيمة المكتوبة حديثًا من الذاكرة الرئيسية. بمعنى آخر ، يمكن للكلمة الرئيسية المتطايرة أن تضمن الرؤية والنظام. لنأخذ الكود أعلاه كمثال:
int a = 0 ؛ bool flag = false ؛ public void write () {a = 2 ؛ // 1 العلم = صحيح ؛ // 2} void public multiply () {if (flag) {// 3 int ret = a * a ؛ // 4}}هذا الرمز ليس مضطربًا فقط من خلال إعادة ترتيب ، حتى لو لم يتم إعادة ترتيب 1 و 2. 3 لن يتم تنفيذها بسلاسة أيضًا. لنفترض أن سلسلة الرسائل 1 تنفذ عملية الكتابة أولاً ، ويقوم مؤشر الترابط 2 ثم يؤدي العملية الضاعفة. نظرًا لأن الموضوع 1 يعين العلامة إلى 1 في الذاكرة العاملة ، فقد لا يتم كتابتها مرة أخرى إلى الذاكرة الرئيسية على الفور. لذلك ، عندما ينفذ الموضوع 2 ، يقرأ مضاعفة قيمة العلامة من الذاكرة الرئيسية ، والتي قد لا تزال خاطئة ، لذلك لن يتم تنفيذ العبارات الموجودة في قوسين. إذا تغيرت إلى ما يلي:
int a = 0 ؛ flatile bool flag = false ؛ public void write () {a = 2 ؛ // 1 العلم = صحيح ؛ // 2} void public multiply () {if (flag) {// 3 int ret = a * a ؛ // 4}}ثم يقوم الموضوع 1 بتنفيذ الكتابة أولاً ، ويتم تنفيذ مؤشر الترابط 2 ثم ينفذ. وفقًا لمبدأ SECT-Be-Frear ، ستفي هذه العملية الأنواع الثلاثة التالية من القواعد: قواعد طلب البرنامج: 1 Select-Beore 2 ؛ 3 يحدث قبل 4 ؛ (يقيد التقلبات تعليمات إعادة ترتيب ، لذلك يتم تنفيذ 1 قبل 2) القواعد المتقلبة: 2 من قبل 3 قواعد متعدية: 1 يحدث قبل 4 عند كتابة متغير متطاير ، سوف JMM مسح المتغير المشترك في الذاكرة المحلية المقابلة للمعلومات إلى الذاكرة الرئيسية. عند قراءة متغير متطاير ، ستقوم JMM بتعيين الذاكرة المحلية المقابلة للمعلومات المتمثلة في إبطالها ، وسيقوم مؤشر الترابط بقراءة المتغير المشترك من الذاكرة الرئيسية التالية.
بادئ ذي بدء ، جوابي هو أنه لا يمكن ضمان الذرة. إذا كان مضمونًا ، فمن مجرد ذرة لقراءة/كتابة متغير متطاير واحد ، ولكن لا يوجد شيء يتعلق بعمليات مركبة مثل Folatile ++ ، مثل المثال التالي:
اختبار الفئة العامة {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 ، ولكن من المحتمل أن تكون قيمة أقل من 10000 عند التشغيل. قد يقول بعض الناس أن التقلب لا يضمن الرؤية. يجب أن يرى مؤشر ترابط واحد التعديلات على Inc بواسطة Inc ، ويجب أن يراها الخيط الآخر على الفور! لكن عملية INC ++ هنا هي عملية مركبة ، بما في ذلك قراءة قيمة INC ، وزيادةها بنفسها ، ثم كتابتها مرة أخرى إلى الذاكرة الرئيسية. افترض أن الخيط A يقرأ قيمة Inc ليكون 10 ، ويتم حظره في هذا الوقت لأنه لم يتم تعديل المتغير ولا يمكن تشغيل القاعدة المتطايرة. يقرأ الموضوع B أيضًا قيمة شركة INC في هذا الوقت. لا تزال قيمة INC في الذاكرة الرئيسية 10 ، وسيتم زيادتها تلقائيًا ، وبعد ذلك سيتم كتابتها مرة أخرى إلى الذاكرة الرئيسية ، التي هي 11 عامًا. في هذا الوقت ، يتحول دور الخيط A إلى التنفيذ. منذ أن تم حفظ 10 في الذاكرة العاملة ، تستمر في زيادة نفسها وتكتب مرة أخرى إلى الذاكرة الرئيسية. 11 مكتوب مرة أخرى. لذلك على الرغم من أن الخيوط التي تم تنفيذها زيادة () مرتين ، إلا أنها أضاف مرة واحدة فقط. بعض الناس يقولون ، ألا يبطئ تقلب خط ذاكرة التخزين المؤقت؟ ومع ذلك ، قبل أن يقرأ الخيط A الخيط B ويقوم بإجراء العمليات ، لا يتم تعديل قيمة INC ، لذلك عندما يقرأ الخيط B ، لا يزال يقرأ 10. بعض الأشخاص يقولون أيضًا أنه إذا كتب الموضوع B 11 إلى الذاكرة الرئيسية ، فلن يقوم بتعيين خط ذاكرة التخزين المؤقت لرسائل الترابط A لإبطاله؟ ومع ذلك ، فإن الموضوع A قد قام بالفعل بعملية القراءة. فقط عند الانتهاء من عملية القراءة وخط ذاكرة التخزين المؤقت غير صالح ، فهل سيقرأ قيمة الذاكرة الرئيسية. لذلك ، يمكن للموضوع A الاستمرار فقط في القيام بالذات. لتلخيص ، في هذا النوع من التشغيل المركب ، لا يمكن الحفاظ على الوظيفة الذرية. ومع ذلك ، في المثال أعلاه لإعداد قيمة العلامة ، نظرًا لأن عملية قراءة/كتابة الأعلام هي خطوة واحدة ، فلا يزال بإمكانها ضمان الذرة. لضمان الذرة ، لا يمكننا إلا استخدام فئات التشغيل المتزامنة والقفل والذري الذرية تحت الحزم المتزامنة ، أي أن المخلصة الذاتية (إضافة 1 عملية) ، و decrease الذاتية (تقليل عملية واحدة) ، وتشغيل الإضافة (إضافة رقم) ، وتشغيل الطرح (طرح رقم واحد) من أنواع البيانات الأساسية لضمان أن تكون هذه العمليات في العمليات.
إذا قمت بإنشاء رمز التجميع مع الكلمة الرئيسية المتطايرة والرمز بدون الكلمة الرئيسية المتطايرة ، فستجد أن الكود الذي يحتوي على الكلمة الرئيسية المتطايرة سيكون له تعليمة بادئة قفل إضافية. تعليمات بادئة القفل تعادل في الواقع حاجز الذاكرة. يوفر حاجز الذاكرة الوظائف التالية: 1. عند إعادة الترتيب ، لا يمكن إعادة ترتيب التعليمات التالية إلى الموقع قبل حاجز الذاكرة 2. قم بإجراء ذاكرة التخزين المؤقت الخاصة بوحدة المعالجة المركزية هذه المكتوبة إلى الذاكرة ** ** 3. سيؤدي إجراء الكتابة أيضًا إلى اتخاذ وحدات المعالجة المركزية الأخرى أو نواة أخرى لإبطال ذاكرة التخزين المؤقت الخاصة بها ، والتي تعادل القيمة المكتوبة الجديدة إلى خيوط أخرى.
1. علامة كمية الحالة ، تمامًا مثل العلم أعلاه ، سأذكرها مرة أخرى:
int a = 0 ؛ flatile bool flag = false ؛ public void write () {a = 2 ؛ // 1 العلم = صحيح ؛ // 2} void public multiply () {if (flag) {// 3 int ret = a * a ؛ // 4}}يمكن أن تضمن عملية القراءة والكتابة للمتغيرات ، التي تم وضع علامة عليها على أنها متقلبة ، أن تكون التعديلات مرئية على الفور للخيط. بالمقارنة مع المزامنة ، فإن القفل لديه تحسن معين من الكفاءة. 2. تنفيذ وضع Singleton ، قفل التحقق المزدوج النموذجي (DCL)
Class Singleton {private platile Static Singleton مثيل = NULL ؛ خاص singleton () {} static singleton getInstance () {if (مثيل == null) {synchronized (singleton.class) {if (exate == null) مثيل = new singleton () ؛ }} مثيل الإرجاع ؛ }}هذا هو نمط Singleton كسول ، ولا يتم إنشاء الكائنات إلا عند استخدامها ، ولتجنب إعادة ترتيب التعليمات لعمليات التهيئة ، تتم إضافة تقلب إلى المثال.
ما سبق هو المحتوى الكامل لهذه المقالة حول شرح الكلمات الرئيسية المتقلبة التي يحبها المقابلات في Java أن يسألوا بالتفصيل. آمل أن يكون ذلك مفيدًا للجميع. يمكن للأصدقاء المهتمين الاستمرار في الرجوع إلى الموضوعات الأخرى ذات الصلة على هذا الموقع. إذا كانت هناك أي أوجه قصور ، فيرجى ترك رسالة لإشارةها. شكرا لك يا أصدقائك لدعمكم لهذا الموقع!