تم ذكر مفهوم خالية من القفل في إدخال [التزامن العالي Java 1]. نظرًا لوجود عدد كبير من التطبيقات الخالية من القفل في رمز مصدر JDK ، يتم تقديم قفل خالية هنا.
1 شرح مفصل لمبدأ الفصل الخالي من القفل
1.1 كاس
عملية خوارزمية CAS هي كما يلي: أنها تحتوي على 3 معلمات CAS (V ، E ، N). يمثل V المتغير المراد تحديثه ، ويمثل E القيمة المتوقعة ، ويمثل N القيمة الجديدة. فقط إذا v
عندما تكون القيمة مساوية لقيمة E ، سيتم تعيين قيمة V على N. إذا كانت قيمة V مختلفة عن قيمة E ، فهذا يعني أن مؤشرات الترابط الأخرى قد أجريت بالفعل تحديثات ، وأن مؤشر الترابط الحالي لا يفعل شيئًا. أخيرًا ، تقوم CAS بإرجاع القيمة الحقيقية لعمليات V. CAS الحالية بموقف متفائل ، ويعتقد دائمًا أنه يمكنه إكمال العمليات بنجاح. عندما تعمل مؤشرات ترابط متعددة متغيرًا باستخدام CAS في نفس الوقت ، سيفوز واحد فقط وتحديث بنجاح ، وسوف يفشل الباقي. لن يتم تعليق الخيط الفاشل ، ويتم إخبارك فقط أن الفشل مسموح به ، ويُسمح له بالمحاولة مرة أخرى ، وبالطبع سيسمح الخيط الفاشل بالتخلي عن العملية. بناءً على هذا المبدأ ، CAS
يتم قفل العملية على الفور ، ويمكن أيضًا اكتشاف مؤشرات الترابط الأخرى التداخل في الخيط الحالي والتعامل معه بشكل مناسب.
سنجد أن هناك العديد من الخطوات في CAS. هل من الممكن أنه بعد الحكم على أن V و E متماثلان ، عندما نكون على وشك تعيين القيم ، قمنا بتبديل مؤشر الترابط وقمنا بتغيير القيمة. ما الذي تسبب في عدم تناسق البيانات؟
في الواقع ، هذا القلق لا لزوم له. عملية التشغيل بأكملها لـ CAS هي عملية ذرية ، يتم الانتهاء منها بواسطة تعليمات وحدة المعالجة المركزية.
1.2 تعليمات وحدة المعالجة المركزية
تعليمات وحدة المعالجة المركزية لـ CAS هي CMMPXCHG
رمز التعليمات كما يلي:
/ * uccumulator = al ، ax ، أو eax ، اعتمادًا على ما إذا كان يتم تنفيذ مقارنة بايت أو كلمة أو كلمات مزدوجة */ if (uccumulator == الوجهة) {zf = 1 ؛ الوجهة = المصدر ؛ } آخر {zf = 0 ؛ تراكم = الوجهة ؛ } إذا كانت القيمة الهدف مساوية للقيمة في السجل ، فسيتم تعيين علامة القفز وتم تعيين البيانات الأصلية على الهدف. إذا لم تنتظر ، فلن تقوم بتعيين علامة القفز.
يوفر Java العديد من الفصول الخالية من القفل ، لذلك دعونا نقدم فصولًا خالية من القفل أدناه.
2 عديمة الفائدة
نحن نعلم بالفعل أن قفل الخالية من القفل أكثر كفاءة من الحجب. دعونا نلقي نظرة على كيفية تنفيذ Java هذه الفصول الخالية من القفل.
2.1. Atomicinteger
Atomicinteger ، مثل عدد صحيح ، يرث كلاهما فئة الأرقام
الطبقة العامة Atomicinteger يمتد الأدوات الأدوات java.io.serializable
هناك العديد من عمليات CAS في AtomicInteger ، والتي هي:
مقارنات منطقية نهائية عامة (int توقع ، int update) {
إرجاع unsafe.compareanswapint (هذا ، valueffset ، توقع ، تحديث) ؛
}
هنا سوف نشرح طريقة Unsafe.compareanswapint. وهذا يعني أنه إذا كانت قيمة المتغير الذي تكون إزاحةه في هذه الفئة هو valueffset هي نفس القيمة المتوقعة ، فقم بتعيين قيمة هذا المتغير على التحديث.
في الواقع ، فإن المتغير مع Valueffset الإزاحة هو القيمة
static {try {valueffset = unsafe.objectfieldoffset (atomicinteger.class.getDeclaredField ("value")) ؛ } catch (استثناء ex) {رمي خطأ جديد (ex) ؛ }}لقد قلنا من قبل أن CAS قد تفشل ، لكن تكلفة الفشل صغيرة جدًا ، وبالتالي فإن التنفيذ العام في حلقة لا حصر لها حتى تنجح.
Public Final int getAndIncrement () {for (؛؛) {int current = get () ؛ int التالي = الحالي + 1 ؛ if (CompareAndSet (الحالي ، التالي)) إرجاع التيار ؛ }}2.2 غير آمن
من اسم الفصل ، يمكننا أن نرى أن العمليات غير الآمنة هي عمليات غير آمنة ، مثل:
اضبط القيمة وفقًا للإزاحة (لقد رأيت هذه الوظيفة في AtomicInteger تم تقديمها للتو)
Park () (أوقف هذا الموضوع ، سيتم ذكره في المدونة المستقبلية)
قد تختلف واجهة برمجة تطبيقات CAS الأساسية غير العامة اختلافًا كبيرًا في إصدارات مختلفة من JDK.
2.3. الذرة
تم ذكر Atomicinteger سابقًا ، وبالطبع Atomicboolean ، Atomiclong ، وما إلى ذلك كلها متشابهة.
ما نريد أن نقدمه هنا هو AtomicReference.
AtomicReference هو فئة قالب
AtomicReference من الطبقة العامة <v> تنفذ java.io.serializable
يمكن استخدامه لتغليف أي نوع من البيانات.
على سبيل المثال ، سلسلة
اختبار الحزمة ؛ استيراد java.util.concurrent.atomic.AtomicReference ؛ اختبار الطبقة العامة {public final static atomicreference <string> AtomicString = new AtomicReference <string> ("Hosee") ؛ public static void main (string [] args) {for (int i = 0 ؛ i <10 ؛ i ++) {final int num = i ؛ new Thread () {public void run () {try {thread.sleep (math.abs ((int) math.random ()*100)) ؛ } catch (استثناء e) {E.PrintStackTrace () ؛ } if (AtomicString.compareAndset ("Hosee" ، "ZTK")) {system.out.println (thread.currentTherD (). getId () + "value value") ؛ } آخر {system.out.println (thread.currentThRead (). getId () + "فشل") ؛ }} ؛ }.يبدأ()؛ }}}نتيجة:
10failed
13 فاصلة
9 تشانج قيمة
11 فاصلة
12 فوضى
15failed
17FALED
14failed
16
18FALED
يمكنك أن ترى أن مؤشر ترابط واحد فقط يمكنه تعديل القيمة ، ولا يمكن لخيوط اللاحق تعديلها بعد الآن.
2.4.AtomicStampedReference
سنجد أنه لا تزال هناك مشكلة في عملية CAS.
على سبيل المثال ، طريقة atomicinteger الإضافية السابقة
public final int guyrementandget () {for (؛؛) {int current = get () ؛ int التالي = الحالي + 1 ؛ إذا كان (CompareAndSet (الحالي ، التالي)) يعود بعد ذلك ؛ }} افترض أن القيمة الحالية = 1 عندما يتم تنفيذ مؤشر ترابط int current = get () ، والتبديل إلى مؤشر ترابط آخر ، يتحول هذا الموضوع 1 إلى 2 ، ثم يتحول مؤشر ترابط آخر إلى 1 إلى 1. في هذا الوقت ، قم بالتبديل إلى الموضوع الأولي. نظرًا لأن القيمة لا تزال تساوي 1 ، لا يزال من الممكن إجراء عمليات CAS. بالطبع ، لا توجد مشكلة في الإضافة. إذا كانت هناك بعض الحالات ، فلن يُسمح بمثل هذه العملية عندما تكون حساسة لحالة البيانات.
في هذا الوقت ، مطلوب فئة AtomicStampedReference.
إنه ينفذ فئة زوج داخليًا لتغليف القيم والجداول الزمنية.
زوج فئة ثابتة خاصة <T> {Final T Reference ؛ ختم int النهائي زوج خاص (مرجع T ، int stamp) {this.reference = reference ؛ this.stamp = Stamp ؛ } static <t> pair <T> من (مرجع t ، int stamp) {return abour new pair <T> (Reference ، Stamp) ؛ }}الفكرة الرئيسية لهذا الفئة هي إضافة الطوابع الزمنية لتحديد كل تغيير.
// مقارنة المعلمات الإعدادات هي: القيمة المتوقعة تكتب القيمة الجديدة توقع الطابع الزمني الجديد
المقارنات المنطقية العامة (v المتوقع ، v newReference ، int equiredStamp ، int newstamp) {pair <V> current = pair ؛ return reportReference == current.reference && repurityStamp == current.stamp && ((newReference == current.reference && newstamp == current.stamp) || caspair (current ، pair.of (NewStamp ، newstamp))) ؛ } عندما تكون القيمة المتوقعة مساوية للقيمة الحالية ويكون الطابع الزمني المتوقع مساوياً للخط الزمني الحالي ، يتم كتابة القيمة الجديدة ويتم تحديث الطابع الزمني الجديد.
فيما يلي سيناريو يستخدم AtomicStampedReference. قد لا يكون هذا مناسبًا ، لكن لا يمكنني تخيل سيناريو جيد.
خلفية المشهد هي أن الشركة تعيد شحن المستخدمين بتوازن منخفض مجانًا ، ولكن يمكن لكل مستخدم إعادة الشحن مرة واحدة فقط.
اختبار الحزمة ؛ استيراد java.util.concurrent.atomic.AtomicStampedReference ؛ اختبار الطبقة العامة {static AtomicStampedReference <integer> money = new AtomicStampedReference <integer> (19 ، 0) ؛ public static void main (string [] args) {for (int i = 0 ؛ i <3 ؛ i ++) {final int timestamp = money.getStamp () ؛ new Thread () {public void run () {whens (true) {when (true) {Integer m = money.getReference () ؛ if (m <20) {if (money.comPareAndset (m ، m + 20 ، timestamp ، timestamp + 1)) {system.out.println ("recharge بنجاح ، الرصيد:" + money.getReference ()) ؛ استراحة؛ }} آخر {break ؛ }}}}}} ؛ }.يبدأ()؛ } new thread () {public void run () {for (int i = 0 ؛ i <100 ؛ i ++) {when (true) {int timestamp = money.getStamp () ؛ عدد صحيح M = money.getReference () ؛ if (m> 10) {if (money.compareanset (m ، m - 10 ، timestamp ، timestamp + 1)) {system.out.println ("المستهلك 10 يوان ، الرصيد:" + money.getReference ()) ؛ استراحة؛ }} آخر {break ؛ }} جرب {thread.sleep (100) ؛ } catch (استثناء e) {// todo: مقبض الاستثناء}}}}} ؛ }.يبدأ()؛ }}اشرح الكود ، هناك 3 مؤشرات ترابط إعادة شحن المستخدم. عندما يكون رصيد المستخدم أقل من 20 ، أعد شحن المستخدم 20 يوان. هناك 100 موضوع مستهلك ، كل ما إنفاق 10 يوان. لدى المستخدم في البداية 9 يوان. عند استخدام AtomicStampedReference لتنفيذها ، سيتم إعادة شحن المستخدم مرة واحدة فقط ، لأن كل عملية تسبب الطابع الزمني +1. نتائج التشغيل:
إعادة الشحن بنجاح ، التوازن: 39
استهلاك 10 يوان ، التوازن: 29
استهلاك 10 يوان ، التوازن: 19
استهلاك 10 يوان ، التوازن: 9
إذا كنت تستخدم AtomicReference <integer> أو عدد صحيح ذري لتنفيذه ، فسيؤدي ذلك إلى إعادة شحن متعددة.
إعادة الشحن بنجاح ، التوازن: 39
استهلاك 10 يوان ، التوازن: 29
استهلاك 10 يوان ، التوازن: 19
إعادة الشحن بنجاح ، التوازن: 39
استهلاك 10 يوان ، التوازن: 29
استهلاك 10 يوان ، التوازن: 19
إعادة الشحن بنجاح ، التوازن: 39
استهلاك 10 يوان ، التوازن: 29
2.5. Atomicintegerarray
بالمقارنة مع AtomicInteger ، فإن تنفيذ المصفوفات هو مجرد ترجمة إضافية.
Public Final Boolean CompareAndset (int i ، int region ، int update) {
Return CompareAndSetraw (checkedbyteoffset (i) ، توقع ، تحديث) ؛
}
الداخلية لها فقط تغلف صفيف عادي
صفيف نهائي خاص []
ما يثير الاهتمام هنا هو أن الأصفار الرائدة للأرقام الثنائية تستخدم لحساب الإزاحة في الصفيف.
Shift = 31 - integer.numberOfLeadzeros (Scale) ؛
يعني الصفر الرائد أنه على سبيل المثال ، تمثل 8 بتات 12،00001100 ، ثم الصفر الرائد هو عدد 0 أمام 1 ، وهو 4.
لم يتم تقديم كيفية حساب الإزاحة هنا.
2.6. Atomicintegerfieldupdater
تتمثل الوظيفة الرئيسية لفئة AtomicIntegerfieldupdater في السماح للمتغيرات العادية بالاستمتاع بالعمليات الذرية أيضًا.
على سبيل المثال ، كان هناك في الأصل متغيرًا كان نوعًا من INT ، وتم تطبيق هذا المتغير في العديد من الأماكن. ومع ذلك ، في سيناريو معين ، إذا كنت ترغب في تحويل نوع int إلى AtomicInteger ، إذا قمت بتغيير النوع مباشرة ، فيجب عليك تغيير التطبيق في أماكن أخرى. تم تصميم AtomicIntegerfieldupdater لحل مثل هذه المشكلات.
اختبار الحزمة ؛ استيراد java.util.concurrent.atomic.atomicinteger ؛ استيراد java.util.concurrent.atomic.atomicintegerfieldupdater ؛ اختبار الفئة العامة {الفئة الثابتة العامة v {int id ؛ تقلبات int. Public Int GetScore () {Return Score ؛ } public void setScore (int score) {this.score = score ؛ }} النهائي العام الثابت atomicintegerfieldupdater <v> vv = atomicintegerfieldupdater.newupdater (V.Class ، "Score") ؛ Atomicinteger alscore alscore الثابت = new AtomicInteger (0) ؛ رميات الفراغ الثابتة العامة (سلسلة [] args) interruptedException {Final v stu = new v () ؛ الموضوع [] t = موضوع جديد [10000] ؛ لـ (int i = 0 ؛ i <10000 ؛ i ++) {t [i] = new thread () {Override public void run () {if (math.random ()> 0.4) {vv.incrementandget (stu) ؛ allscore.incrementandget () ؛ }}} ؛ t [i] .start () ؛ } لـ (int i = 0 ؛ i <10000 ؛ i ++) {t [i] .join () ؛ } system.out.println ("score ="+stu.getScore ()) ؛ System.out.println ("allscore ="+allscore) ؛ }} يتحول الرمز أعلاه إلى درجة باستخدام AtomicIntegerfieldupdater إلى AtomicInteger. ضمان سلامة الموضوع.
يستخدم Allscore هنا للتحقق. إذا كانت قيم النتيجة و allscore هي نفسها ، فهذا يعني أنها آمنة مؤشرات الترابط.
ملحوظة: