أولاً ، دعونا نقدم بعض الأقفال المتفائلة والأقفال المتشائمة:
قفل متشائم: افترض دائمًا أسوأ حالة. في كل مرة أذهب فيها للحصول على البيانات ، أعتقد أن الآخرين سيقومون بتعديلها ، لذلك سأقوم بإغلاقها في كل مرة أحصل فيها على البيانات ، حتى يحظرها الآخرون حتى يحصل على القفل. تستخدم قواعد البيانات العلائقية التقليدية العديد من آليات القفل هذه ، مثل أقفال الصفوف ، وأقفال الجدول ، وما إلى ذلك ، قراءة الأقفال ، وأقفال الكتابة ، وما إلى ذلك ، والتي يتم قفلها قبل إجراء العمليات. على سبيل المثال ، يعد تنفيذ الكلمة الرئيسية المتزامنة للكلمة الرئيسية المتزامنة في Java أيضًا قفلًا متشائمًا.
القفل المتفائل: كما يوحي الاسم ، فهذا يعني متفائل للغاية. في كل مرة أذهب فيها للحصول على البيانات ، أعتقد أن الآخرين لن يقوموا بتعديلها ، لذلك لن أقفلها. ومع ذلك ، عند التحديث ، سأحكم على ما إذا كان الآخرون قد قاموا بتحديث البيانات خلال هذه الفترة ، ويمكنهم استخدام رقم الإصدار والآليات الأخرى. الأقفال المتفائلة مناسبة لأنواع التطبيقات متعددة القراء ، والتي يمكن أن تحسن الإنتاجية. على سبيل المثال ، توفر قاعدة البيانات قفلًا متفائلًا مشابهًا لآلية Write_Condition ، لكنها في الواقع يتم توفيرها جميعها من خلال الأقفال المتفائلة. في Java ، يتم تنفيذ فئة المتغير الذري تحت java.util.concurrent.atomic بواسطة CAS باستخدام قفل متفائل.
تنفيذ القفل المتفائل (مقارنة ومبادلة):
مشاكل القفل:
قبل JDK1.5 ، اعتمدت Java على الكلمات الرئيسية المتزامنة لضمان المزامنة. وبهذه الطريقة ، باستخدام بروتوكول قفل ثابت لتنسيق الوصول إلى الحالة المشتركة ، يمكنه التأكد من أنه بغض النظر عن مؤشر الترابط يحمل قفل المتغيرات المشتركة ، فإنه يستخدم طريقة حصرية للوصول إلى هذه المتغيرات. هذا نوع من القفل الحصري. القفل الحصري هو في الواقع نوع من القفل المتشائم ، لذلك يمكن القول أن المزامنة هو قفل متشائم.
آلية القفل المتشائمة لديها المشاكل التالية:
1. في ظل منافسة متعددة الخيوط ، ستؤدي إضافة أقفال الإضافة والإفراج إلى المزيد من التأخير في التبديل والجدولة ، مما يسبب مشاكل في الأداء.
2. سيؤدي الخيط الذي يحمل القفل إلى حدوث جميع الخيوط الأخرى التي تتطلب تعليق هذا القفل.
3. إذا كان مؤشر ترابط ذي أولوية عالية ينتظر مؤشر ترابط ذي أولوية منخفضة لإطلاق القفل ، فسيؤدي ذلك إلى انعكاس الأولوية ، مما يسبب مخاطر الأداء.
بالمقارنة مع هذه المشكلات من الأقفال المتشائمة ، فإن قفل آخر أكثر فاعلية هو أقفال متفائلة. في الواقع ، القفل المتفائل هو: في كل مرة لا تضيف قفلًا ، لكنك تكمل عملية على افتراض عدم وجود صراع متزامن. إذا فشل الصراع المتزامن ، فحاول مرة أخرى حتى ينجح.
قفل متفائل:
تم ذكر القفل المتفائل أعلاه ، لكنه في الواقع نوع من التفكير. بالمقارنة مع الأقفال المتشائمة ، تفترض الأقفال المتفائلة أن البيانات لن تتسبب عمومًا في تعارضات متزامنة ، لذلك عندما يتم تقديم البيانات وتحديثها ، ستكتشف رسميًا ما إذا كانت البيانات لها تعارضات متزامنة. إذا تم العثور على تعارض متزامن ، فسيتم إرجاع المعلومات الخاطئة للمستخدم وتقرر المستخدم كيفية القيام بذلك.
لقد أوضح مفهوم القفل المتفائل المذكور أعلاه بالفعل تفاصيل التنفيذ الخاصة به: ويتضمن أساسًا خطوتين: اكتشاف الصراع وتحديث البيانات. واحدة من طرق التنفيذ النموذجية هي المقارنة والمبادلة (CAS).
CAS:
CAS هي تقنية قفل متفائلة. عندما تحاول مؤشرات الترابط المتعددة استخدام CAS لتحديث نفس المتغير في نفس الوقت ، يمكن فقط لأحد مؤشرات الترابط تحديث قيمة المتغير ، بينما تفشل مؤشرات الترابط الأخرى. لن يتم تعليق الخيط الفاشل ، ولكن سيتم إخبارهم بأن هذه المسابقة قد فشلت ويمكنها المحاولة مرة أخرى.
تحتوي عملية CAS على ثلاثة معاملات - موقع الذاكرة (V) يجب قراءتها وكتابتها ، والقيمة الأصلية المتوقعة (أ) للمقارنة ، والقيمة الجديدة (ب) التي سيتم كتابتها. إذا تطابق قيمة موضع الذاكرة V القيمة الأصلية المتوقعة ، فسيقوم المعالج تلقائيًا بتحديث قيمة الموضع إلى القيمة الجديدة B. وإلا ، فلن يقوم المعالج بأي شيء. في كلتا الحالتين ، فإنه يعيد قيمة هذا الموقع قبل توجيه CAS. (في بعض الحالات الخاصة لـ CAS ، فقط ما إذا كانت CAS ناجحة أم لا ، دون استخراج القيمة الحالية.) ينص CAS بشكل فعال على أنه "أعتقد أن الموضع V يجب أن يحتوي على القيمة A ؛ إذا كان يحتوي على B في هذا الموقف ؛ وإلا ، لا تغير الموقف ، فقط أخبرني القيمة الحالية لهذا الموضع." هذا هو في الواقع نفس مبدأ فحص الصراع + تحديث بيانات الأقفال المتفائلة.
اسمحوا لي أن أؤكد هنا أن القفل المتفائل هو نوع من التفكير. CAS هي وسيلة لتحقيق هذه الفكرة.
دعم Java لـ CAS:
تم بناء java.util.concurrent الجديد (JUC) في JDK1.5 على CAS. بالمقارنة مع خوارزميات الحظر مثل المزامنة ، يعد CAS تطبيقًا شائعًا لخوارزميات غير الحظر. لذلك ، قامت JUC بتحسين أدائها بشكل كبير.
خذ AtomicInteger في java.util.concurrent كمثال لمعرفة كيفية ضمان سلامة مؤشر الترابط دون استخدام الأقفال. نحن نفهم بشكل أساسي طريقة getandincrement ، والتي تعادل عملية ++ i.
الطبقة العامة AtomicInteger يمتد الأدوات الأدوات java.io.serializable {private folatile int value ؛ public final int get () {return value ؛ } public final int getAndIncrement () {for (؛؛) {int current = get () ؛ int التالي = الحالي + 1 ؛ if (CompareAndSet (الحالي ، التالي)) إرجاع التيار ؛ }} Public Final Boolean CompareAndset (int region ، int update) {return unsafe.compareandswapint (this ، valueffset ، توقع ، تحديث) ؛ }}في الآلية بدون أقفال ، يجب استخدام قيمة الحقل للتأكد من أن البيانات بين مؤشرات الترابط هي الرؤية. وبهذه الطريقة ، يمكنك القراءة مباشرة عندما تحصل على قيمة المتغير. ثم دعونا نرى كيف تم الانتهاء من ++.
يستخدم GetAndIncrement عملية CAS ، وفي كل مرة تقرأ فيها البيانات من الذاكرة ، ثم تنفيذ عملية CAS على هذه البيانات والنتيجة بعد +1. إذا نجحت ، سيتم إرجاع النتيجة ، وإلا حاول مرة أخرى حتى ناجحة.
يستخدم CompareAndset JNI (Java Native Interface) لإكمال تشغيل تعليمات وحدة المعالجة المركزية:
Public Final Boolean CompareAndset (int region ، int update) {return unfafe.compareanswapint (this ، valueoffset ، توقع ، تحديث) ؛ }حيث Unsafe.CompareAndSwapint (هذا ، valueffset ، توقع ، تحديث) ؛ يشبه المنطق التالي:
إذا (هذا == توقع) {هذا = تحديث الإرجاع الحقيقي ؛ } آخر {return false ؛ }إذن كيف تقارن هذا == توقع ، استبدل هذا = التحديث ، ومقارن swapint لتحقيق ذرة هاتين الخطوتين؟ الرجوع إلى مبادئ CAS
مبدأ CAS:
يتم تنفيذ CAS عن طريق استدعاء رمز JNI. يتم تنفيذ CompareAndsWapint باستخدام C للاتصال بتعليمات وحدة المعالجة المركزية الأساسية.
يشرح ما يلي مبدأ التنفيذ لـ CAS من تحليل وحدة المعالجة المركزية الأكثر استخدامًا (Intel X86).
فيما يلي رمز المصدر لطريقة ConpertOndswapint () من Sun.Misc.Unsafe Class:
Public Final Native Boolean CompareAndswapint (Object O ، Long Offset ، int المتوقع ، int x) ؛
يمكنك أن ترى أن هذه مكالمة محلية. رمز C ++ الذي يتصل به هذه الطريقة المحلية في JDK هو:
#define lock_if_mp (mp) __asm cmmp mp ، 0 / __asm je l0 / __asm _emit 0xf0 / __asm l0: inline jint atomic :: cmpxchg (jint exchange_value ، flatile jint* dest ، jint_value) OS :: IS_MP () ؛ __asm {mov edx ، dest mov ecx ، exchange_value mov eax ، compare_value lock_if_mp (mp) cmmpxchg dword ptr [edx] ، ecx}}كما هو موضح في الكود المصدر أعلاه ، سيقرر البرنامج ما إذا كان سيتم إضافة بادئة قفل إلى تعليمة CMMPXCHG استنادًا إلى نوع المعالج الحالي. إذا كان البرنامج يعمل على معالج متعدد ، أضف بادئة القفل إلى تعليمات CMMPXCHG. على العكس من ذلك ، إذا كان البرنامج يعمل على معالج واحد ، يتم حذف بادئة القفل (يحافظ المعالج الفردي نفسه على الاتساق المتسلسل داخل المعالج المفرد ولا يتطلب تأثير حاجز الذاكرة الذي توفره بادئة القفل).
عيوب CAS:
1. أسئلة ABA:
على سبيل المثال ، إذا قام مؤشر ترابط واحد بإخراج A From Memory Potor V ، فإن مؤشر ترابط آخر اثنان يخرجان أيضًا من الذاكرة ، ويقوم اثنان بإجراء بعض العمليات ويصبح B ، ثم يدير اثنان البيانات في الموضع V A. في هذا الوقت ، يقوم مؤشر ترابط One One Operting بعملية CAS ويجد أن A لا يزال في الذاكرة ، ثم يعمل المرء بنجاح. على الرغم من نجاح عملية Thread One CAS ، فقد تكون هناك مشاكل خفية. كما هو موضح أدناه:
هناك مكدس يتم تنفيذه مع قائمة مرتبطة في اتجاه واحد ، مع الجزء العلوي من المكدس هو A. في هذا الوقت ، يعلم الموضوع T1 بالفعل أن A.Next هو B ، ثم يأمل في استبدال الجزء العلوي من المكدس بـ B مع CAS:
head.compareanset (a ، b) ؛
قبل تنفيذ T1 التعليمات أعلاه ، يتدخل مؤشر الترابط T2 ، ويضع A و B خارج المكدس ، ثم PushD و C و A. في هذا الوقت ، هي بنية المكدس كما يلي ، والكائن B في حالة حرة في هذا الوقت:
في هذا الوقت ، حان دور THER T1 لأداء عملية CAS. وجد الكشف أن الجزء العلوي من المكدس لا يزال A ، لذلك ينجح CAS ، ويصبح الجزء العلوي من المكدس B ، ولكن في الواقع B.Next لاغال ، لذلك يصبح الموقف في هذا الوقت:
لا يوجد سوى عنصر واحد في المكدس ، والقائمة المرتبطة المكونة من C و D لم تعد موجودة في المكدس. يتم إلقاء C و D دون سبب.
بدءًا من Java 1.5 ، توفر حزمة JDK الذرية فئة AtomicStampedReference لحل مشكلة ABA. تتمثل طريقة المقارنة في هذه الفئة أولاً في التحقق مما إذا كان المرجع الحالي مساوياً للمرجع المتوقع وما إذا كانت العلامة الحالية مساوية للعلم المتوقع. إذا كانت جميعها متساوية ، يتم تعيين المرجع وقيمة العلم على القيمة المحدثة المحددة بطريقة ذرية.
مقارنات منطقية عامة (V متوقعة ، // المرجع المتوقع v newReference ، // المرجع المحدث int repuritystamp ، // flag inteval newstamp // flag updated)
رمز التطبيق الفعلي:
private static atomicstampedReference <integer> AtomicStampedRef = new AtomicStampedReference <integer> (100 ، 0) ؛ ......... AtomicStampedRef.compareAnset (100 ، 101 ، ختم ، ختم + 1) ؛
2. وقت الدورة الطويل والعالي:
تدور CAS (إذا فشلت ، سيتم تنفيذها تدويرها حتى تنجح) إذا فشلت لفترة طويلة ، فسيؤدي ذلك إلى تنفيذ تنفيذ كبير إلى وحدة المعالجة المركزية. إذا تمكنت JVM من دعم تعليمات الإيقاف المؤقت التي يوفرها المعالج ، فسيتم تحسين الكفاءة إلى حد ما. تعليمات الإيقاف المؤقت لها وظيفتين. أولاً ، يمكن أن يؤخر تعليمات تنفيذ خطوط الأنابيب (De-Pipeline) حتى لا تستهلك وحدة المعالجة المركزية الكثير من موارد التنفيذ. يعتمد وقت التأخير على إصدار التنفيذ المحدد. على بعض المعالجات ، يكون وقت التأخير صفر. ثانياً ، يمكن أن يتجنب تدفق خط أنابيب وحدة المعالجة المركزية الناجم عن انتهاك ترتيب الذاكرة عند الخروج من الحلقة ، وبالتالي تحسين كفاءة تنفيذ وحدة المعالجة المركزية.
3. يمكن ضمان فقط العمليات الذرية للمتغير المشترك:
عند إجراء العمليات على متغير مشترك ، يمكننا استخدام طريقة CAS الدورية لضمان العمليات الذرية. ومع ذلك ، عند تشغيل متغيرات مشتركة متعددة ، لا يمكن لـ CARCAL CAS ضمان ذرة العملية. في هذا الوقت ، يمكنك استخدام الأقفال ، أو أن هناك خدعة ، وهي دمج متغيرات مشتركة متعددة في متغير مشترك للعمل. على سبيل المثال ، هناك متغيرين مشتركان I = 2 ، J = A ، دمج IJ = 2A ، ثم استخدم CAS لتشغيل IJ. بدءًا من Java 1.5 ، توفر JDK فئة AtomicReference لضمان الذرة بين الكائنات المشار إليها. يمكنك وضع متغيرات متعددة في كائن واحد لتشغيل CAS.
CAS وسيناريوهات الاستخدام المتزامنة:
1. بالنسبة للمواقف التي يكون فيها منافسة أقل للموارد (تعارض مؤشر ترابط الضوء) ، فإن استخدام قفل المزامنة المتزامن لحظر مؤشرات الترابط وعمليات التبديل وتبديل الاستيقاظ بين حالات kernel المستخدمة هو مضيعة إضافية لموارد وحدة المعالجة المركزية ؛ في حين يتم تنفيذ CAS استنادًا إلى الأجهزة ، لا تحتاج إلى إدخال kernel ، فلا تحتاج إلى تبديل مؤشرات الترابط ، كما أن فرصة تشغيل الدوران أقل ، لذلك يمكن الحصول على أداء أعلى.
2. بالنسبة للمواقف التي تكون فيها المنافسة في الموارد خطيرة (تعارض خيط شديد) ، يكون احتمال تدور CAS مرتفعًا نسبيًا ، مما يثير المزيد من موارد وحدة المعالجة المركزية وأقل كفاءة من المتزامن.
الملحق: تم تحسين المزامنة وتحسينها بعد JDK1.6. يعتمد التنفيذ الأساسي للتزامن بشكل أساسي على قائمة انتظار القفل. الفكرة الأساسية هي الحظر بعد الدوران ، والاستمرار في التنافس على الأقفال بعد تبديل المنافسة ، والتضحية قليلاً بالإنصاف ، ولكن الحصول على إنتاجية عالية. عندما يكون هناك عدد أقل من تعارضات الخيوط ، يمكن الحصول على أداء مماثل ؛ عندما يكون هناك تعارضات خطيرة في الخيط ، يكون الأداء أعلى بكثير من الأداء من CAS.
تنفيذ الحزمة المتزامنة:
نظرًا لأن Java's CAS لديها كل من دلالات الذاكرة للقراءة المتطايرة والكتابة المتقلبة ، فهناك الآن أربع طرق للتواصل بين خيوط Java:
1. مؤشر الترابط A يكتب المتغير المتطاير ، ثم يقرأ الخيط B المتغير المتطاير.
2. الخيط A يكتب المتغير المتطاير ، ثم يستخدم مؤشر الترابط B CAS لتحديث المتغير المتطاير.
3. يستخدم مؤشر الترابط A CAS لتحديث متغير متطاير ، ثم يستخدم مؤشر الترابط B CAS لتحديث هذا المتغير المتطاير.
4. Thread A يستخدم CAS لتحديث متغير متطاير ، ثم يقرأ الموضوع B هذا المتغير المتطاير.
تستخدم JAVA's CAS التعليمات الذرية على مستوى الجهاز الفعالة المقدمة على المعالجات الحديثة ، والتي تؤدي عمليات القراءة والكتابة على الذاكرة ، وهو مفتاح تحقيق التزامن في المعالجات المتعددة (بشكل أساسي ، فإن آلة الكمبيوتر التي يمكن أن تدعم تعليمات موعدة من القراءة المدعومة ، فإن أي تعليمات غير متجانسة تتسلق. أداء عمليات القراءة الذرية-التغيير والكتابة على الذاكرة). في الوقت نفسه ، يمكن للقراءة/الكتابة و CAS للمتغير المتقلبة تحقيق التواصل بين المواضيع. يشكل دمج هذه الميزات معًا حجر الزاوية في تنفيذ الحزمة المتزامنة بأكملها. إذا قمنا بتحليل تنفيذ رمز المصدر بعناية للحزمة المتزامنة ، فسنجد نمط تنفيذ عام:
1. أولاً ، أعلن أن المتغير المشترك يتقلق ؛
2. ثم ، استخدم تحديث الحالة الذرية لـ CAS لتحقيق التزامن بين المواضيع ؛
3. في نفس الوقت ، يتم تحقيق التواصل بين المواضيع باستخدام قراءة/كتابة متطايرة ودلالات الذاكرة من القراءة المتطايرة والكتابة في CAS.
AQS ، هياكل البيانات غير المحظورة والفئات المتغيرة الذرية (الفئات في java.util.util.concurrent.Atomic Package) ، يتم تنفيذ الفئات الأساسية في هذه الحزم المتزامنة باستخدام هذا النمط ، والفئات عالية المستوى في الحزمة المتزامنة تعتمد على هذه الفئات الأساسية للتنفيذ. من منظور عام ، فإن مخطط تنفيذ الحزمة المتزامنة هو كما يلي:
CAS (تعيين الكائنات في الكومة):
تقوم Java باستدعاء new object() لإنشاء كائن ، والذي سيتم تخصيصه إلى كومة JVM. فكيف يتم حفظ هذا الكائن في الكومة؟
بادئ ذي بدء ، عند تنفيذ new object() ، يتم تحديد المساحة التي يحتاجها هذا الكائن فعليًا ، لأن أنواع البيانات المختلفة في Java ومقدار المساحة التي تتخذها ثابتة (إذا لم تكن واضحًا بشأن مبدأها ، يرجى Google بنفسك). ثم المهمة التالية هي العثور على مساحة في الكومة لتخزين هذا الكائن.
في حالة موضوع واحد ، هناك عمومًا استراتيجيتين تخصيصان:
1. تصادم المؤشر: ينطبق هذا بشكل عام على الذاكرة العادية تمامًا (ما إذا كانت الذاكرة منتظمة تعتمد على استراتيجية إعادة تدوير الذاكرة). تتمثل مهمة تخصيص مساحة في نقل المؤشر مثل مسافة حجم الكائن على جانب الذاكرة الحرة.
2. القائمة الحرة: هذا مناسب للذاكرة غير النظامية. في هذه الحالة ، ستحتفظ JVM بقائمة ذاكرة لتسجيل مناطق الذاكرة مجانية وما هو الحجم. عند تخصيص مساحة للكائنات ، انتقل إلى القائمة المجانية للاستعلام عن المنطقة المناسبة ثم تخصيصها.
ومع ذلك ، من المستحيل على JVM أن يعمل في حالة واحدة مترابطة طوال الوقت ، وبالتالي فإن الكفاءة سيئة للغاية. نظرًا لأنها ليست عملية ذرية عند تخصيص الذاكرة لكائن آخر ، على الأقل ، هناك حاجة إلى الخطوات التالية: العثور على قائمة مجانية ، وتخصيص الذاكرة ، وتعديل قائمة مجانية ، وما إلى ذلك ، وهي غير آمنة. هناك أيضًا استراتيجيتان لحل المشكلات الأمنية أثناء التزامن:
1. CAS: في الواقع ، يستخدم الجهاز الظاهري CAS لضمان ذرية عملية التحديث من خلال الفشل في إعادة المحاولة ، والمبدأ هو نفسه المذكور أعلاه.
2. TLAB: إذا تم استخدام CAS ، فسيكون لها تأثيرًا على الأداء ، وبالتالي اقترح JVM استراتيجية تحسين أكثر تقدماً: كل مؤشر ترابط يسبق جزءًا صغيرًا من الذاكرة في كومة Java ، تسمى المخزن المؤقت لتخصيص الخيط المحلي (TLAB). عندما يحتاج الخيط إلى تخصيص الذاكرة داخلها ، فإنه يكفي تخصيصها مباشرة على TLAB ، وتجنب تعارضات الخيوط. فقط عند استخدام الذاكرة المخزن المؤقت ويحتاج إلى إعادة تخصيص الذاكرة ، سيتم تنفيذ تشغيل CAS لتخصيص مساحة أكبر للذاكرة.
ما إذا كان يمكن تكوين الجهاز الظاهري TLAB من خلال المعلمة -XX:+/-UseTLAB (يتم تمكين الإصدارات اللاحقة والإصدارات المتأخرة افتراضيًا).
ما سبق هو كل محتوى هذه المقالة. آمل أن يكون ذلك مفيدًا لتعلم الجميع وآمل أن يدعم الجميع wulin.com أكثر.