بشكل عام ، يرى المؤلف غالبًا أن العديد من الطلاب يستخدمون فقط بعض الأساليب الأساسية في معالجة نموذج تطوير Java المتزامن. على سبيل المثال ، متقلبة ، متزامنة. غالبًا ما لا يستخدم العديد من الأشخاص الحزم المتزامنة المتقدمة مثل القفل والذرية. أعتقد أن معظم الأسباب بسبب عدم وجود سمات للمبدأ. في أعمال التنمية المزدحمة ، من يمكنه فهم نموذج التزامن الصحيح واستخدامه بدقة؟
في الآونة الأخيرة ، بناءً على هذه الفكرة ، أخطط لتنظيم آلية التحكم في التزامن في مقال. إنها ليست مجرد ذكرى لمعرفتك الخاصة ، ولكنها تأمل أيضًا في أن يساعد المحتوى المذكور في هذه المقالة معظم المطورين.
يتضمن تطوير البرنامج الموازي حتماً قضايا مثل التعاون متعدد الخيوط والتعاون متعدد المهام ومشاركة البيانات. في JDK ، يتم توفير طرق متعددة لتنفيذ التحكم المتزامن بين مؤشرات الترابط المتعددة. على سبيل المثال ، شائع الاستخدام: القفل الداخلي ، قفل إعادة الدخول ، قفل القراءة والكتابة.
نموذج الذاكرة جافا
في Java ، يحتوي كل مؤشر ترابط على منطقة ذاكرة عاملة ، تقوم بتخزين نسخة من قيمة المتغير في الذاكرة الرئيسية المشتركة بواسطة جميع مؤشرات الترابط. عند تنفيذ مؤشر ترابط ، يقوم بتشغيل هذه المتغيرات في ذاكرته العاملة.
من أجل الوصول إلى متغير مشترك ، عادةً ما يكتسب مؤشر ترابط القفل ويزيل منطقة ذاكرة العمل الخاصة به ، مما يضمن تحميل المتغير المشترك بشكل صحيح من منطقة الذاكرة المشتركة لجميع مؤشرات الترابط في منطقة ذاكرة العمل في مؤشر الترابط. عند فتح مؤشر الترابط ، يتم ضمان قيمة المتغير في منطقة الذاكرة العاملة بالذاكرة المشتركة.
عندما يستخدم مؤشر الترابط متغيرًا معينًا ، بغض النظر عما إذا كان البرنامج يستخدم عمليات مزامنة مؤشرات الترابط بشكل صحيح ، يجب أن تكون القيمة التي يحصل عليها هي القيمة المخزنة في المتغير بمفرده أو مؤشرات الترابط الأخرى. على سبيل المثال ، إذا قام اثنان من مؤشر ترابط بتخزين قيم مختلفة أو مراجع كائن في نفس المتغير المشترك ، فإن قيمة المتغير هي إما من هذا الموضوع أو من هذا الموضوع ، ولن تتكون قيمة المتغير المشترك من القيم المرجعية للمعارضين.
عنوان يمكن لبرامج Java الوصول إليه عند استخدام متغير. لا يتضمن فقط متغيرات النوع الأساسي ومتغيرات نوع المرجع ، ولكن أيضًا متغيرات نوع الصفيف. يمكن مشاركة المتغيرات المخزنة في منطقة الذاكرة الرئيسية من قبل جميع مؤشرات الترابط ، ولكن من المستحيل على مؤشر ترابط واحد الوصول إلى المعلمات أو المتغيرات المحلية لخيط آخر ، لذلك لا يتعين على المطورين القلق بشأن مشكلات سلامة مؤشرات الترابط من المتغيرات المحلية.
يمكن رؤية المتغيرات المتطايرة بين خيوط متعددة
نظرًا لأن كل مؤشر ترابط يحتوي على منطقة ذاكرة العمل الخاصة به ، فقد يكون غير مرئي لمؤشرات الترابط الأخرى عندما يغير مؤشر ترابط بيانات ذاكرة العمل الخاصة به. للقيام بذلك ، يمكنك استخدام الكلمة الرئيسية المتطايرة لكسر جميع مؤشرات الترابط لقراءة وكتابة المتغيرات في الذاكرة ، بحيث تكون المتغيرات المتطايرة مرئية بين مؤشرات الترابط المتعددة.
يمكن ضمان المتغيرات المعلنة على أنها متقلبة على النحو التالي:
1. يمكن أن تنعكس تعديلات المتغيرات بواسطة مؤشرات ترابط أخرى على الفور في الخيط الحالي ؛
2. تأكد من أن تعديل مؤشر الترابط الحالي للمتغير المتطاير يمكن أن تُعيد إلى الذاكرة المشتركة في الوقت المناسب ورأيتها من قبل مؤشرات الترابط الأخرى ؛
3. استخدم المتغيرات المعلنة من خلال المتقلبة ، وسيضمن المترجم الخاص بها.
الكلمات الرئيسية المتزامنة
تعد الكلمات الرئيسية المتزامنة المزامنة واحدة من أكثر طرق التزامن شيوعًا في لغة Java. في إصدارات JDK المبكرة ، لم يكن أداء Synchronized جيدًا للغاية ، وكانت القيمة مناسبة للمناسبات التي لم تكن فيها منافسة القفل شرسة بشكل خاص. في JDK6 ، ضاقت الفجوة بين الأقفال المتزامنة وغير العادلة. الأهم من ذلك ، المزامنة أكثر إيجازًا وواضحًا ، والرمز قابل للقراءة والصيانة.
طرق قفل كائن:
طريقة void المزامنة العامة () {}
عندما يتم استدعاء طريقة Method () ، يجب أن يحصل مؤشر ترابط الاتصال أولاً على الكائن الحالي. إذا تم الاحتفاظ بقفل الكائن الحالي بواسطة مؤشرات ترابط أخرى ، فسوف ينتظر مؤشر ترابط الاتصال. بعد انتهاء الانتهاك ، سيتم إصدار قفل الكائن. الطريقة أعلاه تعادل طريقة الكتابة التالية:
طريقة الفراغ العام () {Synchronized (هذا) {// افعل شيئًا ...}} ثانياً ، يمكن أيضًا استخدام المزامنة لبناء كتل المزامنة. بالمقارنة مع طرق التزامن ، يمكن أن تتحكم كتل المزامنة في نطاق رمز التزامن بشكل أكثر دقة. رمز التزامن الصغير سريع جدًا داخل وخارج الأقفال ، مما يمنح النظام إنتاجية أعلى.
طريقة الفراغ العام (كائن O) {// BeForesynchronized (O) {// افعل شيئًا ...} // بعد} يمكن أيضًا استخدام متزامن للوظائف الثابتة:
طريقة الفراغ الثابتة المتزامنة العامة () {}
من المهم أن نلاحظ في هذا المكان إضافة القفل المتزامن إلى كائن الفئة الحالي ، لذلك يجب أن تحصل جميع المكالمات على هذه الطريقة على قفل كائن الفئة.
على الرغم من أن المزامنة يمكن أن تضمن سلامة مؤشر ترابط الكائنات أو مقاطع التعليمات البرمجية ، إلا أن استخدام متزامن وحده لا يزال غير كافٍ للتحكم في تفاعلات مؤشر الترابط مع المنطق المعقد. من أجل تحقيق التفاعل بين مؤشرات الترابط المتعددة ، هناك حاجة أيضًا إلى طرق WAIT () وإخطار () كائن الكائن.
الاستخدام النموذجي:
Synchronized (obj) {بينما (<؟>) {obj.wait () ؛ // تواصل التنفيذ بعد تلقي الإخطار. }} قبل استخدام طريقة Wait () ، تحتاج إلى الحصول على قفل الكائن. عند تنفيذ طريقة WAIT () ، قد يصدر مؤشر الترابط الحالي القفل الحصري لـ OBJ لاستخدامه بواسطة مؤشرات الترابط الأخرى.
عند انتظار أن يتلقى الموضوع على OBJ OBJ.Notify () ، يمكن أن يستعيد قفل OBJ الحصري ومتابعة التشغيل. لاحظ أن طريقة الإخطار () هي استحضار مؤشر ترابط في انتظار الكائن الحالي بشكل عشوائي.
فيما يلي تطبيق قائمة انتظار الحظر:
الفئة العامة blockqueue {قائمة قائمة خاصة = new ArrayList () ؛ كائن متزامن عام pop () يلقي interruptedException {when (list.size () == 0) {this.wait () ؛ } if (list.size ()> 0) {return list.remove (0) ؛ } آخر {return null ؛ }} كائن متزامن عام وضع (Object OBJ) {list.add (obj) ؛ this.notify () ؛ }} يجب أن يكون متزامن وانتظر () وإخطار () مهارة أساسية يجب على مطوري Java إتقانها.
reentrantlock قفل reentrantlock
يسمى Reentrantlock reentrantlock. لديها ميزات أكثر قوة من المزامنة ، يمكن أن تقاطع والوقت. في حالة التزامن العالي ، لديها مزايا واضحة للأداء على المزامنة.
يوفر Reentrantlock أقفال عادلة وغير عادلة. القفل العادل هو الأول في أول من القفل ، ولا يمكن قطع قفل عادل في الطابور. بالطبع ، من منظور الأداء ، فإن أداء الأقفال غير العادلة أفضل بكثير. لذلك ، في غياب الاحتياجات الخاصة ، ينبغي تفضيل الأقفال غير العادلة ، ولكن المزامنة توفر صناعة القفل ليست عادلة للغاية. يمكن لـ REENTRANTLOCK تحديد ما إذا كان القفل عادلًا عند البناء.
عند استخدام قفل إعادة الدخول ، تأكد من إطلاق القفل في نهاية البرنامج. بشكل عام ، يجب كتابة رمز إطلاق القفل في النهاية. خلاف ذلك ، في حالة حدوث استثناء البرنامج ، لن يتم إطلاق Looack أبدًا. يتم إصدار القفل المتزامن تلقائيًا بواسطة JVM في النهاية.
الاستخدام الكلاسيكي كما يلي:
جرب {if (lock.trylock (5 ، timeunit.seconds)) {// إذا تم قفله ، فحاول انتظار 5s لمعرفة ما إذا كان يمكن الحصول على القفل. إذا كان لا يمكن الحصول على القفل بعد 5s ، فأرجع كاذبًا لمتابعة التنفيذ // lock.locklyly () ؛ يمكن الرد على حدث مقاطعة جرب {// العملية} أخيرًا {lock.unlock () ؛ }}} catch (interruptedException e) {E.PrintStackTrace () ؛ // عند مقاطعة الخيط الحالي (مقاطعة) ، سيتم إلقاء interruptedException}يوفر REENTRANTLOCK مجموعة متنوعة غنية من وظائف التحكم في القفل ، ويطبق بشكل مرن طرق التحكم هذه لتحسين أداء التطبيق. ومع ذلك ، لا ينصح بشدة باستخدام reentrantlock هنا. REENTRY LOCK هي أداة تطوير متقدمة مقدمة في JDK.
قراءة القراءة وكتابة القفل
القراءة والكتابة الفصل هي فكرة شائعة معالجة البيانات. يجب أن تعتبر تقنية ضرورية في SQL. ReadWritelock هو قفل فصل القراءة والكتابة المقدمة في JDK5. يمكن أن تساعد أقفال القراءة والكتابة على تقليل منافسة القفل بشكل فعال لتحسين أداء النظام. سيناريوهات الاستخدام لفصل القراءة والكتابة بشكل أساسي إذا كان عدد عمليات القراءة أكبر بكثير من عمليات الكتابة. كيفية استخدامه على النحو التالي:
private reentrantreadwritelock readwritelock = new reentrantreadwritelock () ؛ private lock readlock = readWritelock.readlock () ؛ private lock writelock = readWritelock.writelock () ؛ الكائنات العامة handlerlead () interruptedException {try {readlock.lock () ؛ thread.sleep (1000) ؛ قيمة الإرجاع } أخيرًا {readlock.unlock () ؛ }} الكائنات العامة HandlerEad () يلقي InterruptedException {try {writelock.lock () ؛ thread.sleep (1000) ؛ قيمة الإرجاع } أخيرًا {writelock.unlock () ؛ }} كائن الحالة
يتم استخدام كائن ConditionD لتنسيق التعاون المعقد بين مؤشرات الترابط المتعددة. يرتبط أساسا مع الأقفال. يمكن إنشاء مثيل شرط ملزم بالقفل من خلال طريقة NewCondition () في واجهة القفل. العلاقة بين كائن الشرط والقفل تشبه استخدام وظيفتين كائن. wait () و Object.Notify () والكلمات الرئيسية المتزامنة.
هنا يمكنك استخراج رمز المصدر لـ ArrayBlockingQueue:
يمتد arrayblocking arrayblocking من الطبقة العامة الأدوات التجريدية ، java.io.serializable {/** القفل الرئيسي الذي يحرس جميع الوصول*/قفل إعادة entrantlock النهائي ؛/** الشرط للانتظار ، خذ*/private final condition ؛ غير unalfalArgumentException () ؛ this.items = كائن جديد [السعة] ؛ قفل = جديد reentrantlock (عادلة) ؛ notempty = lock.newcondition () ؛ // إنشاء حالة notfull = lock.newcondition () ؛} public void pum (e e) رميات interruptedException {checkNotnull (e) ؛ القفل النهائي لإعادة الدخول = this.lock ؛ lock.lockInterruptly () ؛ حاول {بينما (count == items.length) notfl.await () ؛ إدراج (هـ) ؛ } أخيرًا {lock.unlock () ؛ }} private void insert (e x) {items [putIndex] = x ؛ putIndex = inc (putIndex) ؛ ++ العد ؛ notempty.signal () ؛ // الإخطار} public e take () رميات interruptedException {final reentrantlock lock = this.lock ؛ lock.lockInterruptly () ؛ حاول {بينما (count == 0) // إذا كانت قائمة الانتظار فارغة notempty.await () ؛ // ثم يجب أن تنتظر قائمة انتظار المستهلك لاستخراج إرجاع إشارة غير فارغ () ؛ } أخيرًا {lock.unlock () ؛ }} extract extract () {Final Object [] final [] = this.items ؛ e x = this. <e> cast (العناصر [takeIndex]) ؛ العناصر [takeIndex] = null ؛ TakeIndex = Inc (takeIndex) ؛ --عدد؛ notfull.signal () ؛ // إخطار PUT () بأن قائمة انتظار مؤشر الترابط تحتوي على مساحة خالية X ؛} // رمز آخر} يوفر Semaphore Semaphore <BR /> Semaphore طريقة تحكم أكثر قوة للتعاون متعدد الخيوط. Semaphore هو امتداد للقفل. سواء أكان هذا هو القفل الداخلي المتماسك أو reentrantlock ، يسمح مؤشر ترابط واحد بالوصول إلى مورد في وقت واحد ، في حين يمكن لسيارة الإشارة تحديد أن مؤشرات الترابط المتعددة تصل إلى مورد في نفس الوقت. من المنشئ ، يمكننا أن نرى:
الإشارة العامة (تصاريح INT) {}
يمكن أن تحدد الإشارة العامة (INT تصاريح ، معرض منطقي) {} // ما إذا كان عادلاً
يحدد التصاريح كتاب الوصول إلى Semaphore ، مما يعني عدد التراخيص التي يمكن تطبيقها في نفس الوقت. عندما ينطبق كل مؤشر ترابط فقط للحصول على ترخيص واحد في وقت واحد ، فإن هذا يعادل تحديد عدد المواضيع التي يمكنها الوصول إلى مورد معين في نفس الوقت. فيما يلي الطرق الرئيسية للاستخدام:
Bublic Void Acquire () رميات InterruptedException {} // حاول الحصول على إذن وصول. إذا لم يكن متاحًا ، فسينتظر مؤشر الترابط ، مع العلم أن مؤشر ترابط يطلق إذنًا أو تم مقاطعة الخيط الحالي.
Public Void AcquireUnInterruptive () {} // على غرار الحصول على () ، لكنه لا يستجيب للمقاطعات.
Boolean Public TryAcquire () {} // حاول الحصول عليها ، صحيحة إذا نجحت ، بطريقة خاطئة. هذه الطريقة لن تنتظر وستعود على الفور.
يلقي Boolean TryAcquire العام (فترة طويلة ، وحدة الوقت) interruptedException {} // كم من الوقت يستغرق الانتظار
يتم استخدام إصدار Public Void () // لإصدار ترخيص بعد اكتمال مورد الوصول في الموقع حتى تتمكن مؤشرات الترابط الأخرى التي تنتظر الإذن من الوصول إلى المورد.
دعنا نلقي نظرة على أمثلة استخدام الرسائل المتوفرة في مستند JDK. يشرح هذا المثال كيفية التحكم في الوصول إلى الموارد من خلال Semaphores.
Public Class Pool {private static Final int max_available = 100 ؛ semaphore النهائي الخاص متاح = semaphore جديد (max_available ، true) ؛ الكائن العام getItem () remrows interruptedException {Available.acquire () ؛ // تقدم بطلب للحصول على ترخيص // فقط 100 مؤشر ترابط يمكن أن يدخل للحصول على العناصر المتاحة في نفس الوقت ، // إذا كان أكثر من 100 ، فأنت بحاجة إلى انتظار الإرجاع getNextAvailableItem () ؛} public pustiTem (Object x) {// وضع العنصر المحدد في التجمع والاحتفال به على أنه (markasunused (x)) {متاح. // أضفت عنصرًا متاحًا ، وأصدر ترخيصًا ، ويتم تنشيط مؤشر الترابط الذي يطلب المورد}} // على سبيل المثال المرجع فقط ، كائن محمي البيانات غير الواقعية [] عناصر = كائن جديد [max_available] ؛ // المستخدمة في كائنات الكائنات المتعددة الكائنات المحمية Boolean [] المستخدمة = New Boolean [max_available] ؛ // وظيفة markup محمية كائن متزامن getNextAvailableItem () {for (int i = 0 ؛ i <max_available ؛ ++ i) {if (! use [i]) {use [i] = true ؛ إرجاع العناصر [i] ؛ }} return null ؛} محمية markasunused markasunused (عنصر كائن) {for (int i = 0 ؛ i <max_available ؛ ++ i) {if (item == items [i]) {if (use [i]) {use [i] = false ؛ العودة صحيح. } آخر {return false ؛ }}} return false ؛}} يقوم هذا المثيل ببساطة بتنفيذ تجمع كائن بسعة أقصى قدره 100. لذلك ، عندما يكون هناك 100 طلب كائن في نفس الوقت ، سيكون لمجموعة الكائنات نقص في الموارد ، ويحتاج مؤشرات الترابط التي تفشل في الحصول على الموارد إلى الانتظار. عندما ينتهي مؤشر ترابط باستخدام كائن ، يحتاج إلى إرجاع الكائن إلى تجمع الكائن. في هذا الوقت ، نظرًا لزيادة الموارد المتاحة ، يمكن تنشيط موضوع ينتظر المورد.
Threadlocal Thread المتغيرات المحلية <br /> بعد البدء في الاتصال بـ ThreadLocal ، من الصعب بالنسبة لي أن أفهم سيناريوهات استخدام هذا المتغير المحلي. عند النظر إلى الوراء الآن ، يعد ThreadLocal حلاً للوصول المتزامن إلى المتغيرات بين مؤشرات الترابط المتعددة. على عكس طرق القفل المتزامنة وغيرها ، لا يوفر ThreadLocal أقفال على الإطلاق ، ولكنه يستخدم طريقة تبادل مساحة للوقت لتزويد كل مؤشر ترابط بنسخ مستقلة من المتغيرات لضمان سلامة مؤشرات الترابط. لذلك ، ليس حلاً لمشاركة البيانات.
Threadlocal هي فكرة جيدة لحل مشاكل السلامة في الخيط. هناك خريطة في فئة threadlocal التي تخزن نسخة من المتغيرات لكل مؤشر ترابط. مفتاح العنصر في الخريطة هو كائن مؤشر ترابط ، والقيمة تتوافق مع نسخة المتغيرات للمعلومات. نظرًا لأنه لا يمكن تكرار قيمة المفتاح ، فإن كل "كائن مؤشر ترابط" يتوافق مع "نسخة المتغيرات" من مؤشر الترابط ، ويصل إلى سلامة مؤشر الترابط.
إنه جدير بالملاحظة بشكل خاص. من حيث الأداء ، ليس لدى ThreadLocal أداء مطلق. عندما لا يكون حجم التزامن مرتفعًا جدًا ، سيكون أداء القفل أفضل. ومع ذلك ، كمجموعة من حلول آمنة مؤشرات الترابط التي لا علاقة لها تمامًا بالأقفال ، يمكن أن يؤدي استخدام ThreadLocal إلى تقليل منافسة القفل إلى حد ما في التزامن العالي أو المنافسة الشرسة.
فيما يلي استخدام بسيط لـ ThreadLocal:
الفئة العامة TestNum {// Overtrite threadlocal's initialValue () من خلال الفئة الداخلية المجهولة ، حدد القيمة الأولية seqnum seqnum = new threadlocal () {public integer initialValue () {return 0 ؛ }} ؛ // احصل على قيمة التسلسل التالية العامة int getNextNum () {seqnum.set (seqnum.get () + 1) ؛ return seqnum.get () ؛} public static void main (string [] args) {testnum sn = new testnum () ؛ // 3 threads share sn ، كل توليد رقم تسلسل testClient t1 = new testClient (sn) ؛ TestClient T2 = new testClient (SN) ؛ TestClient T3 = TestClient جديد (SN) ؛ t1.start () ؛ t2.start () ؛ t3.start () ؛ } testclient الفئة الثابتة الخاصة يمتد Thread {private testnum sn ؛ TestClient العام (testnum sn) {this.sn = sn ؛ } public void run () {for (int i = 0 ؛ i <3 ؛ i ++) {// كل مؤشر ترابط ينتج 3 قيم تسلسل system.out.println ("thread [" + thread.currentThRead (). getName () + "] -> sn }}}} نتيجة الإخراج:
الموضوع [Thread-0]> SN [1]
الموضوع [Thread-1]> SN [1]
الموضوع [Thread-2]> SN [1]
الموضوع [Thread-1]> SN [2]
الموضوع [Thread-0]> SN [2]
الموضوع [Thread-1]> SN [3]
الموضوع [Thread-2]> SN [2]
الموضوع [Thread-0]> SN [3]
الموضوع [Thread-2]> SN [3]
يمكن العثور على معلومات نتيجة الإخراج أنه على الرغم من أن أرقام التسلسل التي تم إنشاؤها بواسطة كل مؤشر ترابط تشترك في نفس مثيل TestNum ، إلا أنها لا تتداخل مع بعضها البعض ، ولكن كل منها يولد أرقام تسلسل مستقلة. هذا لأن ThreadLocal يوفر نسخة منفصلة لكل مؤشر ترابط.
يعد أداء القفل وتحسين "الأقفال" أحد أكثر طرق التزامن شيوعًا. في التطوير العادي ، يمكنك في كثير من الأحيان رؤية العديد من الطلاب يضيفون مباشرة جزءًا كبيرًا من الكود إلى القفل. يمكن لبعض الطلاب استخدام طريقة قفل واحدة فقط لحل جميع مشاكل المشاركة. من الواضح أن هذا الترميز غير مقبول. خاصة في بيئات التزامن العالية ، ستؤدي منافسة القفل الشرسة إلى تدهور أداء أكثر وضوحًا للبرنامج. لذلك ، يرتبط الاستخدام العقلاني للأقفال مباشرة بأداء البرنامج.
1. الخيط النفقات العامة <BR /> في حالة Multi-Core ، يمكن أن يؤدي استخدام متعدد الخيوط إلى تحسين أداء النظام بشكل كبير. ومع ذلك ، في المواقف الفعلية ، سيؤدي استخدام متعدد الخيوط إلى إضافة نظام إضافي للنظام. بالإضافة إلى استهلاك الموارد لمهام النظام أحادي النواة نفسها ، تحتاج التطبيقات متعددة الخيوط أيضًا إلى الحفاظ على معلومات فريدة متعددة الخيوط. على سبيل المثال ، بيانات تعريف مؤشر الترابط نفسه ، جدولة مؤشرات الترابط ، تبديل سياق مؤشرات الترابط ، إلخ.
2. تقليل وقت القفل
في البرامج التي تستخدم الأقفال للتحكم المتزامن ، عندما تتنافس الأقفال ، فإن وقت الاحتفاظ بالقفل لخيط واحد له علاقة مباشرة مع أداء النظام. إذا كان الخيط يحتفظ بالقفل لفترة طويلة ، فستكون المنافسة على القفل أكثر كثافة. لذلك ، أثناء عملية تطوير البرنامج ، يجب تقليل وقت احتلال قفل معين لتقليل إمكانية الاستبعاد المتبادل بين المواضيع. على سبيل المثال ، الكود التالي:
syncmehod () {beforemethod () ؛ mutexmethod () ؛ mutexmethod () ؛ mutexmethod () ؛ eftmethod () إذا كانت طريقة mutexmethod () فقط في هذه الحالة متزامنة ، ولكن في beforemethod () و eftmethod () لا تتطلب التحكم في التزامن. إذا كانت BeForemethod () و AfterMethod () هي طرق الوزن الثقيل ، فسوف يستغرق الأمر وقتًا طويلاً في وحدة المعالجة المركزية. في هذا الوقت ، إذا كان التزامن كبيرًا ، فإن استخدام مخطط التزامن هذا سيؤدي إلى زيادة كبيرة في خيوط الانتظار. نظرًا لأن سلسلة الرسائل التي تنفذ حاليًا لن تصدر القفل إلا بعد تنفيذ جميع المهام.
فيما يلي حل محسّن ، والذي يزامنه فقط عند الضرورة ، بحيث يمكن تقليل وقت احتجاز مؤشرات الترابط بشكل كبير ويمكن تحسين إنتاجية النظام. الرمز كما يلي:
syncmehod () public void syncmehod () {beforemethod () ؛ Synchronized (this) {mutexmethod () ؛} eftmethod () ؛} 3. قلل من حجم جسيم القفل
يعد تقليل الحبيبات القفل وسيلة فعالة أيضًا لإضعاف المنافسة على الأقفال متعددة الخيوط. سيناريو الاستخدام النموذجي لهذه التكنولوجيا هو فئة concurrenthashmap. في hashmap العادية ، كلما تم تنفيذ عملية Add () أو GET () على مجموعة ، يتم دائمًا الحصول على قفل كائن المجموعة. هذه العملية هي سلوك متزامن تمامًا لأن القفل موجود على كائن المجموعة بأكمله. لذلك ، في التزامن العالي ، ستؤثر منافسة القفل الشرسة على إنتاجية النظام.
إذا كنت قد قرأت الكود المصدري ، فيجب أن تعلم أنه يتم تنفيذ HashMap في قائمة مرتبطة + صفيف +. يقسم ConcurrentHashMap كامل الهاشماب إلى عدة قطاعات (قطاعات) ، وكل جزء عبارة عن شخص فرعي. إذا كنت بحاجة إلى إضافة إدخال جدول جديد ، فأنت لا تقفل hashmap. سيحصل خط البحث العشرين على القسم الذي يجب أن يتم فيه تخزين إدخال الجدول وفقًا لرمز Hashcode ، ثم قفل القسم وإكمال عملية PUT (). وبهذه الطريقة ، في بيئة متعددة الخيوط ، إذا كانت مؤشرات ترابط متعددة تقوم بعمليات الكتابة في نفس الوقت ، طالما أن العنصر الذي يتم كتابته غير موجود في نفس الجزء ، فيمكن تحقيق التوازي الحقيقي بين المواضيع. للتنفيذ المحدد ، آمل أن يستغرق القراء بعض الوقت لقراءة الكود المصدري لفئة ConcurrentHashMap ، لذلك لن أصفها كثيرًا هنا.
4. قفل فصل <br /> قراءة القراءة والكتابة المذكورة سابقًا ، ثم تمديد فصل القراءة والكتابة هو فصل القفل. يمكن أيضًا العثور على رمز المصدر لفصل القفل في JDK.
الطبقة العامة LinkedBlockingQueue يمتد أدوات تجريدية java.io.serializable {/*lock hold by take ، old ، etc/private reentrantlock takelock = new reentrantlock () PUTLOCK = جديد reentrantlock () ؛/** انتظر قائمة الانتظار للانتظار يضع*/private Final Condition Notfull = putlock.newcondition () ؛ public e take () remrows interruptedException {ex ؛ int c = -1 ؛ عدد AtomicInteger النهائي = this.count ؛ النهائي reentrantlock takelock = this.takelock ؛ takelock.lockInterruptly () ؛ // لا يمكن أن يكون هناك موضوعان لقراءة البيانات في نفس الوقت حاول {بينما (count.get () == 0) {// إذا لم تكن هناك بيانات متاحة ، فانتظر إخطار put () notempty.await () ؛ } x = dequeue () ؛ // إزالة عنصر c = count.getAndDecrement () ؛ // size minus 1 if (c> 1) notempty.signal () ؛ // إخطار Other Take () عمليات} أخيرًا {takelock.unlock () ؛ // regle lock} if (c == captic) signalNotfull () ؛ . // ملاحظة: الاتفاقية في جميع put/take/etc هي إعداد VAR // المحلية المحلية السلبية للإشارة إلى الفشل ما لم يتم تعيينها. int c = -1 ؛ العقدة <e> node = new node (e) ؛ reentrantlock النهائي putlock = this.putlock ؛ عدد AtomicInteger النهائي = this.count ؛ putlock.lockInterruptly () ؛ // لا يمكن أن يكون هناك موضوعان يضعان البيانات في نفس الوقت حاول { / * * لاحظ أن العد يستخدم في واقي الانتظار على الرغم من أنه غير محمي بواسطة القفل. يعمل هذا لأن العد لا يمكن أن ينخفض فقط في هذه المرحلة (يتم إغلاق جميع الطبقات الأخرى * عن طريق القفل) ، ونحن (أو بعض وضع الانتظار الآخر) موقعة * إذا تغيرت من السعة. وبالمثل * لجميع الاستخدامات الأخرى للعد في حراس الانتظار الآخرين. */ بينما (count.get () == السعة) {// إذا كانت قائمة الانتظار ممتلئة ، فانتظر notfl.await () ؛ } enqueue (عقدة) ؛ // انضم إلى قائمة الانتظار C = count.getandincrement () ؛ // size plus 1 if (c + 1 <cappy) notull.signal () ؛ // قم بإخطار مؤشرات الترابط الأخرى إذا كان هناك مساحة كافية} أخيرًا {putlock.unlock () ؛ // قم بإطلاق القفل} إذا كان (c == 0) signalNotempty () ؛ // بعد أن نجح الإدراج ، قم بإخطار take () لقراءة البيانات} // رمز آخر}}ما يجب توضيحه هنا هو أن وظائف Take () و () مستقلة عن بعضها البعض ، ولا توجد علاقة منافسة القفل بينهما. ما عليك سوى التنافس على Takelock و Putlock ضمن طرق Take () و Put (). وبالتالي ، فإن إمكانية منافسة القفل تضعف.
5. قفل الخشنة <br /> يتم إجراء التخفيض المذكور أعلاه لوقت القفل والتحسينات لتلبية أقصر وقت لكل موضوع لعقد القفل. ومع ذلك ، يجب أن يتم فهم درجة في التفاصيل. إذا تم طلب القفل باستمرار ومزامنة وإطلاق سراحه ، فسيستهلك موارد قيمة للنظام وزيادة النفقات العامة للنظام.
ما نحتاج إلى معرفته هو أنه عندما يواجه جهاز افتراضي سلسلة من الطلبات والإصدارات المستمرة من نفس القفل ، فإنه سيدمج جميع عمليات القفل في طلب واحد إلى القفل ، مما يقلل من عدد طلبات القفل. وتسمى هذه العملية قفل الخشنة. فيما يلي مظاهرة لمثال التكامل:
public void syncmehod () {synchronized (lock) {method1 () ؛} synchronized (lock) {method2 () ؛}} النموذج بعد تكامل JVM: public void syncmehod () {synchronized (lock) {method1 () ؛ method2 () ؛}}}لذلك ، يمنح هذا التكامل مطورينا تأثيرًا توضيحيًا جيدًا على فهم الحبيبات.
لقد أمضت الحوسبة المتوازية غير الملائمة <br /> ما ورد أعلاه الكثير من الوقت في الحديث عن القفل ، وذكر أيضًا أن القفل سيؤدي إلى إدخال موارد إضافية لتبديل سياق معين. في التزامن العالي ، قد تصبح المنافسة الشرسة على "قفل" عنق الزجاجة. لذلك ، يمكن استخدام طريقة التزامن غير الحظر هنا. لا يزال بإمكان هذه الطريقة الخالية من القفل ضمان الحفاظ على الاتساق بين المواضيع المتعددة في بيئة تزامن عالية.
1. التزامن غير الحظر/قفل
تنعكس طريقة التزامن غير المحظورة فعليًا في Threadlocal السابقة. كل مؤشر ترابط له نسخة مستقلة خاصة به من المتغيرات ، لذلك ليست هناك حاجة لانتظار بعضها البعض عند الحوسبة بالتوازي. هنا ، يوصي المؤلف بشكل أساسي بطريقة التحكم في التزامن خالية من القفل بشكل أساسي استنادًا إلى خوارزمية CASS ومبادلة CAS.
عملية خوارزمية CAS: أنها تحتوي على 3 معلمات CAS (V ، E ، N). يمثل V المتغير المراد تحديثه ، ويمثل E القيمة المتوقعة ، ويمثل N القيمة الجديدة. سيتم ضبط قيمة V على n فقط عندما تكون قيمة V مساوية لقيمة E. إذا كانت قيمة V مختلفة عن قيمة E ، فهذا يعني أن مؤشرات الترابط الأخرى قد أجرت تحديثات ، وأن الخيط الحالي لا يفعل شيئًا. أخيرًا ، تقوم CAS بإرجاع القيمة الحقيقية لـ V. عندما تستخدم مؤشرات ترابط متعددة CAS لتشغيل متغير في نفس الوقت ، سيتم فوز واحد فقط ويتم تحديثه بنجاح ، بينما يفشل بقية Junhui. لن يتم تعليق الخيط الفاشل ، ويتم إخبارك فقط أن الفشل مسموح به ، ويُسمح له بالمحاولة مرة أخرى ، وبالطبع سيسمح الخيط الفاشل بالتخلي عن العملية. استنادًا إلى هذا المبدأ ، فإن عملية CAS في الوقت المناسب بدون أقفال ، ويمكن أيضًا اكتشاف مؤشرات الترابط أيضًا التداخل في الخيط الحالي والتعامل معه بشكل مناسب.
2. عملية الوزن الذري
توفر حزمة JDK's Java.util.concurrent.atomic فئات التشغيل الذرية التي يتم تنفيذها باستخدام خوارزميات خالية من القفل ، ويستخدم الرمز بشكل أساسي تطبيق الكود الأصلي الأساسي. يمكن للطلاب المهتمين متابعة تتبع رمز المستوى الأصلي. لن أنشر تطبيق رمز السطح هنا.
يستخدم ما يلي مثالًا بشكل أساسي لإظهار فجوة الأداء بين طرق التزامن العادية والمزامنة الخالية من القفل:
الفئة العامة testatomic {private static int int max_threads = 3 ؛ private static int task_count = 3 ؛ private static int target_count = 100 * 10000 ؛ private AtomicInteger account = new AtomicInteger (0) Runnable {string name ؛ وقت البدء الطويل ؛ testatomic خارج. SyncThRead العام (testatomic o ، وقت بدء طويل) {this.out = o ؛ this.startTime = وقت البدء ؛ } Override public void run () {int v = out.inc () ؛ بينما (v <target_count) {v = out.inc () ؛ } long endtime = system.currentTimeMillis () ؛ System.out.println ("SyncThread Ength:" + (Endtime - StartTime) + "MS" + "، v =" + V) ؛ }} الفئة العامة AtomicThRead تنفذ Runnable {string name ؛ وقت البدء الطويل ؛ Public AtomicThread (وقت بدء طويل) {this.startTime = StartTime ؛ } Override public void run () {int v = account.incrementandget () ؛ بينما (v <target_count) {v = account.incrementandget () ؛ } long endtime = system.currentTimeMillis () ؛ System.out.println ("AtomicThread Ength:" + (Endtime - StartTime) + "MS" + "، v =" + v) ؛ }}@testpublic void testsync () رميات interruptedException {executorService exe = Executors.NewFixedThreadPool (max_threads) ؛ وقت بدء طويل = system.currentTimeMillis () ؛ SyncThread Sync = New SyncThread (هذا ، وقت البدء) ؛ لـ (int i = 0 ؛ i <task_count ؛ i ++) {exe.submit (sync) ؛ } thread.sleep (10000) ؛}@testpublic void testatomic () يلقي interruptedException {executorService exe = executors.newfixedthreadpool (max_threads) ؛ وقت بدء طويل = system.currentTimeMillis () ؛ AtomicThread Atomic = new AtomicThread (وقت البدء) ؛ لـ (int i = 0 ؛ i <task_count ؛ i ++) {exe.submit (Atomic) ؛ } thread.sleep (10000) ؛}} نتائج الاختبار كما يلي:
testSync ():
SyncThread Engnmn: 201ms ، v = 1000002
SyncThread Engnmn: 201ms ، v = 1000000
SyncThread إنفاق: 201ms ، v = 1000001
testatomic ():
تنفق AtomicThread: 43 مللي ثانية ، v = 1000000
تنفق AtomicThread: 44 مللي ثانية ، v = 1000001
تنفق AtomicThread: 46 مللي ثانية ، v = 1000002
أعتقد أن نتائج الاختبار هذه ستعكس بوضوح الاختلافات في الأداء بين خوارزميات التزامن الداخلية غير المحظورة. لذلك ، يوصي المؤلف بشكل مباشر اعتبار هذه الفئة الذرية تحت Atomic.
خاتمة
أخيرًا ، قمت بفرز الأشياء التي أريد التعبير عنها. في الواقع ، لا تزال هناك بعض الفصول مثل CountDownlatch التي لم يتم ذكرها. ومع ذلك ، فإن ما ذكر أعلاه هو بالتأكيد جوهر البرمجة المتزامنة. ربما يستطيع بعض القراء رؤية الكثير من نقاط المعرفة هذه على الإنترنت ، لكن ما زلت أعتقد أنه بالمقارنة يمكن العثور على المعرفة في سيناريو الاستخدام المناسب. لذلك ، هذا هو السبب أيضًا في قيام المحرر بتجميع هذا المقال ، وآمل أن يساعد هذا المقال المزيد من الطلاب.
ما سبق هو كل محتوى هذه المقالة. آمل أن يكون ذلك مفيدًا لتعلم الجميع وآمل أن يدعم الجميع wulin.com أكثر.