سلسلة البرمجة Java Concurrent [غير مكتملة]:
• برمجة تزامن جافا: النظرية الأساسية
• البرمجة المتزامنة Java: متزامنة ومبادئ تنفيذها
• البرمجة المتزامنة Java: التحسين الأساسي المتزامن (قفل خفيف الوزن ، قفل متحيز)
• البرمجة المتزامنة Java: التعاون بين المواضيع (الانتظار/الإخطار/النوم/العائد/الانضمام)
• البرمجة المتزامنة Java: استخدام المتقلبة ومبادئها
1. دور المتقلبة
في مقالة "برمجة تزامن جافا: النظرية الأساسية" ، ذكرنا مشاكل الرؤية والنظام والذرة. عادة ، يمكننا حل هذه المشكلات من خلال الكلمة الرئيسية المتزامنة. ومع ذلك ، إذا كان لديك فهم لمبدأ المزامنة ، فيجب أن تعلم أن المزامنة هي عملية ثقيلة نسبيًا ولها تأثير كبير نسبيًا على أداء النظام. لذلك ، إذا كانت هناك حلول أخرى ، فإننا عادة ما نتجنب استخدام متزامن لحل المشكلة. الكلمة الرئيسية المتطايرة هي حل آخر يتم توفيره في Java لحل مشاكل الرؤية والنظام. فيما يتعلق بالذرة ، من النقطة أيضًا أن يكون الجميع عرضة لسوء الفهم: يمكن أن تضمن تشغيل القراءة/الكتابة الفردية للمتغيرات المتطايرة الذرة ، مثل متغيرات النوع الطويل والمزدوج ، ولكن لا يمكن أن تضمن ذرة عمليات I ++ ، لأنه في جوهرها ، يتم قراءة وتكتب عمليات الكتابة مرتين.
2. استخدام متطاير
فيما يتعلق باستخدام متقلبة ، يمكننا استخدام عدة أمثلة لتوضيح استخدامه وسيناريوهات.
1. منع إعادة ترتيب
دعنا نحلل مشكلة إعادة الترتيب من أحد أكثر الأمثلة الكلاسيكية. يجب أن يكون الجميع على دراية بتنفيذ نموذج Singleton ، وفي بيئة متزامنة ، يمكننا عادةً استخدام طريقة قفل الفحص المزدوج (DCL) لتنفيذها. رمز المصدر كما يلي:
حزمة com.paddx.test.concurrent ؛ الطبقة العامة singleton {public static folatile singleton singleton ؛ / *** المُنشئ خاصًا ، ويحظر الاستئصال الخارجي*/ خاص singleton () {} ؛ Singleton Singleton GetInstance () {if (singleton == null) {synchronized (singleton) {if (singleton == null) {singleton = new Singleton () ؛ }} إرجاع singleton ؛ }}الآن دعنا نحلل لماذا نحتاج إلى إضافة الكلمة الرئيسية المتطايرة بين متغير Singleton. لفهم هذه المشكلة ، يجب أولاً فهم عملية بناء الكائنات. يمكن تقسيم كائن ما إلى ثلاث خطوات:
(1) تخصيص مساحة الذاكرة.
(2) تهيئة الكائن.
(3) تعيين عنوان مساحة الذاكرة إلى المرجع المقابل.
ومع ذلك ، نظرًا لأن نظام التشغيل يمكنه إعادة ترتيب التعليمات ، فقد تصبح العملية أعلاه أيضًا العملية التالية:
(1) تخصيص مساحة الذاكرة.
(2) تعيين عنوان مساحة الذاكرة إلى المرجع المقابل.
(3) تهيئة الكائن
إذا كانت هذه العملية هي العملية ، فقد يتم تعريض مرجع كائن غير ضروري في بيئة متعددة الخيوط ، مما يؤدي إلى نتائج غير متوقعة. لذلك ، لمنع إعادة ترتيب هذه العملية ، نحتاج إلى ضبط المتغير على متغير من النوع المتطاير.
2. تحقيق الرؤية
تشير مشكلة الرؤية بشكل أساسي إلى مؤشر ترابط واحد يعدل القيمة المتغيرة المشتركة ، بينما لا يمكن لخيط آخر رؤيته. السبب الرئيسي لمشكلة الرؤية هو أن كل مؤشر ترابط لديه منطقة ذاكرة التخزين المؤقت الخاصة به - ذاكرة العمل. يمكن للكلمة الرئيسية المتطايرة حل هذه المشكلة بفعالية. دعونا نلقي نظرة على الأمثلة التالية لمعرفة وظيفتها:
package com.paddx.test.concurrent ؛ فئة publatiletest {int a = 1 ؛ int b = 2 ؛ تغيير الفراغ العام () {a = 3 ؛ ب = أ ؛ } public void print () {system.out.println ("b ="+b+"؛ a ="+a) ؛ } public static void main (string [] args) {بينما (true) {Final FlatiLetest test = new Folatiletest () ؛ مؤشر ترابط جديد (جديد RunNable () {Override public void run () {try {thread.sleep (10) ؛} catch (interruptedException e) {E.PrintStackTrace () ؛} test.change () ؛}). start () ؛ مؤشر ترابط جديد (جديد RunNable () {Override public void run () {try {thread.sleep (10) ؛} catch (interruptedException e) {e.printstacktrace () ؛} test.print () ؛}}). start () ؛ }}}من الناحية حدسي ، لا يوجد سوى نتائج محتملة لهذا الرمز: B = 3 ؛ A = 3 أو B = 2 ؛ A = 1. ومع ذلك ، تشغيل الكود أعلاه (ربما يستغرق وقتًا أطول قليلاً) ، ستجد أنه بالإضافة إلى النتيجة السابقة ، هناك أيضًا نتيجة ثالثة:
...... b = 2 ؛ a = 1b = 2 ؛ a = 1b = 3 ؛ a = 3b = 3 ؛ a = 3b = 3 ؛ a = 1b = 3 ؛ a = 3b = 2 ؛ a = 1b = 3 ؛ a = 3b = 3 ؛ a = 3b = 3 ؛ a = 3 ...
لماذا تظهر نتيجة مثل B = 3 ؛ A = 1؟ في ظل الظروف العادية ، إذا قمت بتنفيذ طريقة التغيير أولاً ثم قم بتنفيذ طريقة الطباعة ، فيجب أن تكون نتيجة الإخراج B = 3 ؛ A = 3. على العكس من ذلك ، إذا قمت بتنفيذ طريقة الطباعة أولاً ثم قم بتنفيذ طريقة التغيير ، فيجب أن تكون النتيجة B = 2 ؛ A = 1. إذن كيف تخرج نتيجة B = 3 ؛ A = 1؟ والسبب هو أن الخيط الأول يعدل القيمة A = 3 ، ولكن غير مرئي للمعلومات الثانية ، لذلك تحدث هذه النتيجة. إذا تم تغيير كلا من A و B إلى متغيرات من النوع المتطاير وتنفيذها ، فلن تظهر نتيجة B = 3 ؛ A = 1 مرة أخرى.
3. ضمان الذرة
تم شرح مسألة الذرة أعلاه. يمكن أن يضمن التقلب فقط ذرة واحدة للقراءة/الكتابة. يمكن وصف هذه المشكلة في JLS:
17.7 المعالجة غير الآلية للمزدوجة والطويلة لأغراض نموذج ذاكرة لغة برمجة Java ، يتم التعامل مع كتابة واحدة على قيمة طويلة أو مزدوجة غير متطايرة ككتابين منفصلين: واحد إلى كل نصف بتات 32 بت. يمكن أن يؤدي ذلك إلى موقف حيث يرى مؤشر ترابط أول 32 بت من قيمة 64 بت من كتابة واحدة ، والثانية 32 بت من الكتابة الأخرى. تكتب وقراءة القيم الطويلة والمزدوجة المتطايرة دائمًا. تكتب وقراءات المراجع دائمًا ذرية ، بغض النظر عما إذا كانت قد تم تنفيذها كقيم 32 بت أو 64 بت. قد تجد بعض التطبيقات أنه من المريح تقسيم إجراء كتابة واحد على قيمة طويلة أو مزدوجة 64 بت إلى اثنين من إجراءات الكتابة على قيم 32 بت المجاورة. من أجل الكفاءة ، هذا السلوك خاص بالتنفيذ ؛ إن تنفيذ الجهاز الظاهري Java مجاني لأداء الكتابة على قيم طويلة ومزدوجة ذرية أو في جزأين. يتم تشجيع تطبيقات الجهاز الافتراضي Java على تجنب تقسيم قيم 64 بت حيثما أمكن ذلك. يتم تشجيع المبرمجين على الإعلان عن قيم 64 بت مشتركة على أنها متقلبة أو تزامن برامجهم بشكل صحيح لتجنب compplications المحتملة.
يشبه محتوى هذا المقطع تقريبًا ما وصفته سابقًا. نظرًا لأن عمليات النوعين من البيانات الطويلة والمزدوجة يمكن تقسيمها إلى جزأين: 32 بت و 32 بت منخفضة ، قد لا تكون أنواع طويلة أو مزدوجة ذرية. لذلك ، يتم تشجيع الجميع على تعيين المتغيرات الطويلة والمزدوجة المشتركة على أنواع متقلبة ، والتي يمكن أن تضمن أن عمليات القراءة/الكتابة الفردية ذات الطول والمزدوج هي ذرية في أي حال.
هناك مشكلة تضمن المتغيرات المتقلبة الذرة ، والتي يساء فهمها بسهولة. الآن سنظهر هذه المشكلة من خلال البرنامج التالي:
package com.paddx.test.concurrent ؛ فئة publatileTest01 {folatile int i ؛ public void addi () {i ++ ؛ } rems static void main (string [] args) remrows interruptedException {Final FolatiLetest01 test01 = new Folatiletest01 () ؛ لـ (int n = 0 ؛ n <1000 ؛ n ++) {new thread (new RunNable () {Override public void run () {try {thread.sleep (10) ؛} catch (interruptedException e) { } thread.sleep (10000) ؛ // انتظر لمدة 10 ثوان للتأكد من أن تنفيذ البرنامج أعلاه قد اكتمل system.out.println (test01.i) ؛ }}قد تعتقد عن طريق الخطأ أنه بعد إضافة الكلمة الرئيسية المتطايرة إلى المتغير I ، هذا البرنامج آمن مؤشر ترابط. يمكنك محاولة تشغيل البرنامج أعلاه. فيما يلي نتائج المدى المحلي:
ربما يدير الجميع النتائج بشكل مختلف. ومع ذلك ، ينبغي أن نرى أن المتقلبة لا يمكن أن تضمن الذرة (وإلا يجب أن تكون النتيجة 1000). السبب بسيط جدا جدا. I ++ هي في الواقع عملية مركبة ، بما في ذلك ثلاث خطوات:
(1) اقرأ قيمة i.
(2) إضافة 1 إلى أنا.
(3) اكتب قيمة أعود إلى الذاكرة.
ليس هناك ما يضمن أن هذه العمليات الثلاث ذرية. يمكننا التأكد من ذرة عمليات +1 من خلال AtomicInteger أو متزامن.
ملاحظة: تم تنفيذ طريقة thread.sleep () في العديد من الأماكن في الأقسام المذكورة أعلاه من الكود ، بهدف زيادة فرصة حدوث مشاكل في التزامن وليس له أي تأثير آخر.
3. مبدأ التقلب
من خلال الأمثلة المذكورة أعلاه ، يجب أن نعرف بشكل أساسي ما هو التقلب وكيفية استخدامه. الآن دعونا نلقي نظرة على كيفية تنفيذ الطبقة الأساسية من المتقلبة.
1. تنفيذ الرؤية:
كما هو مذكور في المقالة السابقة ، لا يتفاعل مؤشر الترابط نفسه بشكل مباشر مع بيانات الذاكرة الرئيسية ، ولكنه يكمل العمليات المقابلة من خلال ذاكرة العمل في مؤشر الترابط. هذا هو أيضًا السبب الأساسي الذي تجعل البيانات بين المواضيع غير مرئية. لذلك ، لتحقيق رؤية المتغيرات المتقلبة ، يمكنك البدء مباشرة من هذا الجانب. هناك اختلافان رئيسيان بين عمليات الكتابة على المتغيرات المتقلبة والمتغيرات العادية:
(1) عند تعديل المتغير المتطاير ، سيتم إجبار القيمة المعدلة على تحديث الذاكرة الرئيسية.
(2) تعديل المتغير المتطاير سيؤدي إلى فشل القيم المتغيرة المقابلة في الذاكرة العاملة للمواضيع الأخرى. لذلك ، عند قراءة قيمة هذا المتغير مرة أخرى ، تحتاج إلى قراءة القيمة في الذاكرة الرئيسية مرة أخرى.
من خلال هاتين العمليتين ، يمكن حل مشكلة رؤية المتغيرات المتقلبة.
2. التنفيذ المنظم:
قبل شرح هذه المشكلة ، دعونا أولاً نفهم القواعد التي تحدث في Java. تعريف الحدث قبل JSR 133 كما يلي:
يمكن طلب إجراءين من خلال العلاقة التي تحدث قبل ذلك. إذا حدث إجراء واحد قبل آخر ، يكون الأول مرئيًا وطلب قبل الثانية.
بعبارات الرجل العادي ، إذا حدث قبل b ، فإن أي عمليات A مرئية لـ B. (يجب أن يتذكر الجميع هذا ، لأن الكلمة التي تحدث قبل سوء فهمها بسهولة كما كان من قبل وبعدها). دعونا نلقي نظرة على ما يتم تعريف القواعد التي تحدث قبل JSR 133:
• يحدث كل إجراء في مؤشر ترابط قبل كل إجراء لاحق في هذا الموضوع. • يحدث فتح على الشاشة قبل كل قفل لاحق على تلك الشاشة. • تحدث الكتابة إلى حقل متقلبة قبل كل قراءة لاحقة لتلك المتقلبة. • تحدث مكالمة للبدء () على مؤشر ترابط قبل أي إجراءات في موضوع البدء. • تحدث جميع الإجراءات في مؤشر ترابط قبل أن تعود أي مؤشر ترابط آخر بنجاح من Join () في هذا الموضوع. • في حالة حدوث إجراء A قبل إجراء B ، و B قبل إجراء C ، فإن A يحدث قبل c.
ترجم على النحو التالي:
• العملية السابقة تحدث في نفس الموضوع. (على سبيل المثال ، في مؤشر ترابط واحد ، من القانوني تنفيذها في ترتيب الكود. ومع ذلك ، يمكن للمترجم والمعالج إعادة ترتيب دون التأثير على نتائج التنفيذ في بيئة واحدة مترابطة. وبعبارة أخرى ، لا يمكن أن تضمن القواعد إعادة ترتيب التجميع وإعادة ترتيب التعليمات).
• فتح العملية على الشاشة التي تحدث قبل عملية القفل اللاحقة. (القواعد المتزامنة)
• اكتب عملية إلى متغير متقلبة يحدث قبل عمليات القراءة اللاحقة. (قواعد متطايرة)
• طريقة start () من مؤشر الترابط يحدث قبل جميع العمليات اللاحقة للمعلومات. (قاعدة بدء الموضوع)
• جميع عمليات الخيوط التي تحدث قبل أن تنضم إلى مؤشرات الترابط الأخرى في هذا الموضوع وإرجاع العملية الناجحة.
• إذا حدث قبل b ، b ، pele-be-be-be-be-be-pelect ، فعندئذٍ c-pe-be-be-be-pe-be (transitive).
هنا ننظر أساسًا إلى القاعدة الثالثة: قواعد ضمان ترتيب المتغيرات المتقلبة. ذكرت مقالة "برمجة Java Concurrency: النظرية الأساسية" أن إعادة ترتيبها مقسمة إلى إعادة ترتيب برنامج التحويل البرمجي وإعادة ترتيب المعالج. لتنفيذ دلالات الذاكرة المتقلبة ، يقيد JMM إعادة ترتيب هذين النوعين من المتغيرات المتطايرة. فيما يلي جدول قواعد إعادة ترتيب المحددة بواسطة JMM للمتغيرات المتطايرة:
| يمكن إعادة ترتيب | العملية الثانية | |||
| العملية الأولى | الحمل العادي المتجر العادي | الحمل المتقلوب | متجر متقلبة | |
| الحمل العادي المتجر العادي | لا | |||
| الحمل المتقلوب | لا | لا | لا | |
| متجر متقلبة | لا | لا | ||
3. حاجز الذاكرة
من أجل تنفيذ الرؤية المتطايرة ودلالات الحدوث. يتم JVM الأساسي من خلال شيء يسمى "حاجز الذاكرة". حاجز الذاكرة ، المعروف أيضًا باسم سياج الذاكرة ، هو مجموعة من تعليمات المعالج المستخدمة لتنفيذ قيود متسلسلة على عمليات الذاكرة. فيما يلي حاجز الذاكرة المطلوب لإكمال القواعد المذكورة أعلاه:
| الحواجز المطلوبة | العملية الثانية | |||
| العملية الأولى | الحمل العادي | المتجر العادي | الحمل المتقلوب | متجر متقلبة |
| الحمل العادي | LoadStore | |||
| المتجر العادي | Storestore | |||
| الحمل المتقلوب | تحميل | LoadStore | تحميل | LoadStore |
| متجر متقلبة | storeload | Storestore | ||
(1) حاجز تحميل
ترتيب التنفيذ: load1―> loadload―> load2
تأكد من أن تعليمات LOAD2 والتحميل اللاحقة يمكنها الوصول إلى البيانات التي تم تحميلها بواسطة LOAD1 قبل تحميل البيانات.
(2) حاجز Storestore
أمر التنفيذ: store1―> storestore―> store2
تأكد من أن بيانات تشغيل STORE1 مرئية للمعالجات الأخرى قبل STORE2 وتنفيذ تعليمات المتجر اللاحقة.
(3) حاجز LoadStore
أمر التنفيذ: load1―> loadStore―> Store2
تأكد من أنه قبل تنفيذ تعليمات المتجر والمتجر اللاحقة ، يمكن الوصول إلى البيانات التي تم تحميلها بواسطة LOAD1.
(4) حاجز Storeload
أمر التنفيذ: store1―> storeload―> load2
تأكد من قراءة تعليمات التحميل اللاحقة والتحميل اللاحقة ، فإن بيانات STORE1 مرئية للمعالجات الأخرى.
أخيرًا ، يمكنني استخدام مثال لتوضيح كيفية إدخال حاجز الذاكرة في JVM:
حزمة com.paddx.test.concurrent ؛ الطبقة العامة memorybarrier {int a ، b ؛ تقلب int v ، u ؛ void f () {int i ، j ؛ أنا = أ ؛ j = b ؛ أنا = v ؛ // loadload j = u ؛ // loadstore a = i ؛ ب = ي ؛ // storestore v = i ؛ // storestore u = j ؛ // storeload i = u ؛ // loadload // loadstore j = b ؛ أ = أنا ؛ }}4. ملخص
بشكل عام ، لا يزال فهم التقلبات صعبًا نسبيًا. إذا كنت لا تفهم ذلك بشكل جيد ، فلن تضطر إلى التعجيل. يتطلب الأمر عملية لفهمها تمامًا. سترى أيضًا سيناريوهات استخدام المتقلبة عدة مرات في المقالات اللاحقة. هنا لدي فهم أساسي للمعرفة الأساسية للمتقلب والأصل. بشكل عام ، التقلب هو تحسين في البرمجة المتزامنة ، والتي يمكن أن تحل محل متزامن في بعض السيناريوهات. ومع ذلك ، لا يمكن أن يحل المتقلبة محل موضع متزامن. فقط في بعض السيناريوهات الخاصة يمكن تطبيقها. بشكل عام ، يجب تلبية الشرطين التاليين في نفس الوقت لضمان سلامة الخيط في بيئة متزامنة:
(1) لا تعتمد عملية الكتابة للمتغيرات على القيمة الحالية.
(2) لا يتم تضمين هذا المتغير في المتغيرات الأخرى.
المقالة أعلاه حول البرمجة Java Conversion: استخدام ProTile و Benergle هو كل المحتوى الذي أشاركه معك. آمل أن تتمكن من إعطائك مرجعًا وآمل أن تتمكن من دعم wulin.com أكثر.