ما هي الأقفال في جافا
لم أتمكن من الإجابة على هذا السؤال بعد قراءة <Java Conversion Programming> ، مما يدل على أنني لا أفهم ما يكفي عن مفهوم الأقفال. لذلك نظرت من خلال محتويات الكتاب مرة أخرى وشعرت فجأة أنني فتحت جبهتي. يبدو أن أفضل طريقة للتعلم هي التعلم بالمشاكل وحلها.
في Java ، هناك نوعان رئيسيان من الأقفال: قفل داخلي متزامن وعرض قفل java.util.concurrent.locks.lock. ولكن إذا فكرت في الأمر بعناية ، فيبدو أن الملخص غير صحيح. يجب أن تكون سلسلة من الأقفال التي تنفذها الأقفال المدمجة Java ومتزامنة.
لماذا هذا يقول ، لأنه في جافا كل شيء هو كائن ، وجافا لديها قفل مدمج في كل كائن ، والذي يمكن أن يطلق عليه أيضًا قفل كائن/قفل داخلي. يتم الانتهاء من عملية القفل ذات الصلة من خلال المزامنة.
بسبب العيوب في تنفيذ متزامن وتعقيد السيناريوهات المتزامنة ، قام شخص ما بتطوير قفل صريح ، وتستمد هذه الأقفال من java.util.concurrent.locks.locks. بالطبع ، تم بناؤه في JDK1.5 والإصدارات الأحدث.
متزامن
أولاً ، دعنا نلقي نظرة على المزامنة التي يتم استخدامها بشكل متكرر. كما أنه يستخدم في عملي اليومي. يتم استخدام Synchronized لتوفير آلية قفل لكتلة رمز معينة. سيكون لها ضمني قفل في كائنات جافا. يسمى هذا القفل أقفال جوهرية أو مراقبة. يحصل مؤشر الترابط تلقائيًا على هذا القفل قبل إدخال الكتلة المحمية بواسطة المزامنة ، حتى يتم إصدار القفل تلقائيًا بعد اكتمال الرمز (أو قد يكون أيضًا استثناءً). الأقفال المدمجة هي حصرية بشكل متبادل. لا يمكن الاحتفاظ بقفل إلا من خلال مؤشر ترابط واحد في نفس الوقت ، مما سيؤدي أيضًا إلى مؤشرات ترابط متعددة ، وسيتم حظر الخيوط وراء القفل بعد الاحتفاظ بها. هذا يتيح سلامة مؤشر ترابط الكود لضمان الذرة.
إعادة الدخول
نظرًا لأن الأقفال المدمجة Java هي حصرية بشكل متبادل وستتسبب الخيوط اللاحقة في انسداد ، ماذا يحدث إذا دخل الخيط الذي يحمل القفل مرة أخرى عند محاولة الحصول على القفل؟ على سبيل المثال ، واحدة من الحالات التالية:
الفئة العامة baseclass {public synchronized void do () {system.out.println ("is base") ؛ }} الفئة العامة SonClass تمتد BaseClass {public synchronized void do () {system.out.println ("is son") ؛ super.do () ؛ }} sonclass son = new sonclass () ؛ son.do () ؛في هذا الوقت ، ستحتفظ طريقة DO بالفئة المشتقة أولاً ، ثم أدخل القفل مرة أخرى ويحملها عند الاتصال Super.do (). إذا كان القفل حصريًا بشكل متبادل ، فيجب أن يكون مسدود في هذا الوقت.
لكن النتيجة ليست هي الحالة ، لأن القفل الداخلي له سمة من خلال إعادة الدخول ، أي أن القفل ينفذ آلية إعادة إدخال ، وإدارة العدد المرجعي. عندما يحتفظ مؤشر الترابط 1 بقفل الكائن A ، سيتم حساب المرجع إلى القفل A بإضافة 1. ثم عندما يحصل الموضوع 1 على قفل A مرة أخرى ، لا يزال الموضوع 1 يحمل القفل A ، ثم سيضيف الحساب 1. بالطبع ، في كل مرة تقوم فيها بالخروج من كتلة التزامن ، سيتم تقليله بمقدار 1 حتى يتم 0.
بعض ميزات المزامنة
كيفية تعديل الكود
طريقة التعديل
الفئة العامة baseclass {public synchronized void do () {system.out.println ("is base") ؛ }}هذا يعني قفل طريقة مباشرة ، وتحتاج إلى الحصول على قفل عند إدخال كتلة الطريقة هذه.
تعديل كتل الرمز
الفئة العامة baseclass {private static object lock = new Object () ؛ public void do () {synchronized (lock) {system.out.println ("is base") ؛ }}}هنا ، يتم تقليل نطاق القفل إلى بعض كتل التعليمات البرمجية في الطريقة ، مما يحسن مرونة القفل. بعد كل شيء ، فإن التحكم الحبيبي في القفل هو أيضًا مشكلة رئيسية للقفل.
نوع قفل الكائن
غالبًا ما أرى أن بعض الرموز تستخدم متزامنة من حيث الشروط الخاصة ، وننظر إلى الكود التالي:
الفئة العامة baseclass {private static object lock = new Object () ؛ public void do () {synchronized (lock) {}} void dovoid dovoid () {} متزامن عام static void dostaticvoid () {}هناك أربع حالات هنا: تعديل كتلة التعليمات البرمجية ، وتعديل الطريقة ، وتعديل الطريقة الثابتة ، وتعديل كائن الفئة من Baseclass. إذن ما هي الاختلافات في هذه الحالات؟
تعديل كتل الرمز
في هذه الحالة ، نقوم بإنشاء قفل كائن ، باستخدام (قفل) متزامن في الكود ، مما يعني استخدام قفل الكائن المدمج. في هذه الحالة ، يتم تسليم التحكم في القفل إلى كائن. بالطبع هناك طريقة أخرى للقيام بذلك:
public void do () {synchronized (this) {system.out.println ("is base") ؛ }}باستخدام هذا يعني قفل الكائن الحالي. تم ذكر مفتاح القفل المدمج هنا أيضًا. أقدم قفلًا لحماية هذا الرمز. بغض النظر عن الخيط الذي يأتي ، سيواجه نفس القفل.
طريقة لتعديل الكائنات
ما هو الوضع مع هذا التعديل المباشر؟ في الواقع ، تشبه كتل التعليمات البرمجية ، باستثناء أن هذا هو قفل الكائن الحالي بشكل افتراضي. وبهذه الطريقة ، يكون من الواضح نسبيًا كتابة الرمز. كما ذكرنا سابقًا ، فإن الفرق بين تعديل كتل التعليمات البرمجية هو الفرق بين التحسينات.
تعديل الطرق الثابتة
هل هناك أي شيء مختلف عن الأساليب الثابتة؟ إنه مختلف بالفعل. لم يعد هذا القفل الذي تم الحصول عليه في هذا الوقت هذا ، والفصل الذي أشار إليه هذا الكائن هو قفل الفئة. نظرًا لأن معلومات الفصل في Java سيتم تحميلها في المنطقة الثابتة للأسلوب ، فإن العالم فريد من نوعه. هذا في الواقع يوفر قفل عالمي.
كائن فئة للفئة المعدلة
هذا الموقف يشبه في الواقع عند تعديل الأساليب الثابتة ، لكنه لا يزال نفس السبب. يمكن أن توفر هذه الطريقة تفاصيل تحكم أكثر مرونة.
ملخص
من خلال تحليل هذه المواقف وفهمها ، يمكننا أن نرى في الواقع أن المفهوم الأساسي الرئيسي للقفل المدمج هو توفير قطعة من التعليمات البرمجية مع قفل يمكن استخدامه للحصري بشكل متبادل ، ويلعب وظيفة مشابهة للمفتاح.
توفر Java أيضًا بعض التطبيقات للأقفال المدمجة. الميزة الرئيسية هي أن Java هي جميع الكائنات ، وأن كل كائن يحتوي على قفل ، بحيث يمكنك اختيار القفل الذي يجب استخدامه وفقًا للموقف.
java.util.concurrent.locks.lock
نظرت إلى المزامنة في وقت سابق. في معظم الحالات ، يكاد يكون ذلك كافيًا. ومع ذلك ، أصبح النظام أكثر تعقيدًا في البرمجة المتزامنة ، لذلك هناك دائمًا العديد من السيناريوهات التي ستكون المعالجة المتزامنة أكثر صعوبة. أو كما هو مذكور في <Java Conversion Programming> ، تعد الأقفال المتزامنة مكملة للأقفال الداخلية ، مما يوفر ميزات أكثر تقدمًا.
تحليل بسيط لـ java.util.concurrent.locks.lock
تقوم هذه الواجهة بتجريد العملية الرئيسية للقفل ، وبالتالي تسمح الأقفال المستمدة من القفل للحصول على هذه الخصائص الأساسية: غير مشروط ، قابلة للدورة ، قابلة للتطبيق ، قابلة للمقاطعة. علاوة على ذلك ، يتم تنفيذ عمليات القفل وإلغاء القفل بشكل صريح. هنا رمزها:
الواجهة العامة قفل {void lock () ؛ Lockinterruptly () رميات المقاطع ؛ Boolean Trylock () ؛ Boolean Trylock (الوقت الطويل ، وحدة الوقت) رميات المقاطعات ؛ inflock void () ؛ حالة newCondition () ؛} reentrantlock
REENTRANTLOCK هو قفل إعادة إدخال ، حتى الاسم واضح للغاية. يوفر REENTRANTLOCK دلالات مماثلة للمزامنة ، ولكن يجب أن تسمى إعادة الدخول بشكل صريح ، مثل:
الفئة العامة baseclass {private lock lock = new reentrantlock () ؛ public void do () {lock.lock () ؛ حاول {// ..} أخيرًا {lock.unlock () ؛ }}}هذه الطريقة واضحة تمامًا لقراءة التعليمات البرمجية ، ولكن هناك مشكلة ، أي إذا نسيت إضافة محاولة أخيرًا أو تنسى كتابة القفل. لا يوجد خطر المزامنة.
Trylock
تقوم Reentrantlock بتنفيذ واجهة القفل ، لذلك لها بطبيعة الحال ميزاتها ، بما في ذلك Trylock. Trylock هو محاولة الحصول على القفل. إذا تم احتلال القفل من قبل مؤشرات ترابط أخرى ، فسوف يعود على الفور خطأ. إذا لم يكن الأمر كذلك ، فيجب أن يتم احتلاله وإرجاعه صحيحًا ، مما يعني أنه تم الحصول على القفل.
طريقة أخرى Trylock تحتوي على معلمات. تتمثل وظيفة هذه الطريقة في تحديد وقت ، مما يعني أنك تستمر في محاولة الحصول على القفل خلال هذا الوقت ، والاستسلام إذا لم يتم الحصول على الوقت.
نظرًا لأن Trylock لا يمنع دائمًا وينتظر الأقفال ، يمكن أن يتجنب حدوث Deadlocks أكثر.
قفل
يستجيب Lockinterruptluredruptluredruptive للمقاطعات عندما تكتسب الخيوط أقفال. إذا تم الكشف عن مقاطعة ، يتم إلقاء استثناء المقاطعة بواسطة رمز الطبقة العليا. في هذه الحالة ، يتم توفير آلية خروج لقفل روبن مستدير. من أجل فهم عملية القفل القابلة للمقاطعة بشكل أفضل ، تمت كتابة عرض تجريبي لفهمه.
package com.test ؛ import java.util.date ؛ import java.util.concurrent.locks.reentrantlock ؛ public class testlockly {static reentrantlock lock = new reentrantlock () ؛ public static void main (string [] args) {thread thread1 = new thread (new RunNable () {Override public void run () {try {doprint ("thread 1 get lock. Thread Thread2 = New Thread (New RunNable () {Override public void run () {try {doprint ("thread 2 get lock. thread1.setName ("thread1") ؛ thread2.setName ("thread2") ؛ thread1.start () ؛ حاول {thread.sleep (100) ؛ // انتظر لفترة من الوقت لجعل thread1 تنفيذها أمام thread2} catch (interruptedException e) {E.PrintStackTrace () ؛ } thread2.start () ؛ } private static void do123 () رميات interruptedException {lock.lockInterruptilely () ؛ doPrint (thread.currentThRead (). getName () + "مقفل.") ؛ حاول {doPrint (thread.currentThRead (). getName () + "dosoming1 ....") ؛ thread.sleep (5000) ؛ // انتظر بضع ثوان لعرض ترتيب المواضيع doprint (thread.currentThread (). getName () + "dosoming2 ....") ؛ doPrint (thread.currentThRead (). getName () + "انتهى.") ؛ } أخيرًا {lock.unlock () ؛ }} private static void doPrint (سلسلة سلسلة) {system.out.println ((date date ()). tolocalestring () + ":" + text) ؛ }}هناك نوعان من المواضيع في الكود أعلاه. يبدأ Thread1 في وقت مبكر من Thread2. من أجل رؤية عملية القفل ، ينام الرمز المقفل لمدة 5 ثوانٍ ، بحيث يمكنك أن تشعر بعملية الخيوط التي تدخل عملية الاستحواذ على القفل. النتيجة النهائية للرمز أعلاه هي كما يلي:
2016-9-28 15:12:56: الموضوع 1 احصل على قفل.
2016-9-28 15:12:56: تم تأمين Thread1.
2016-9-28 15:12:56: Thread1 dosoming1 ....
2016-9-28 15:12:56: Thread 2 Get Lock.
2016-9-28 15:13:01: Thread1 dosoming2 ....
2016-9-28 15:13:01: تم الانتهاء من Thread1.
2016-9-28 15:13:01: تم تفريغ Thread1.
2016-9-28 15:13:01: تم تأمين Thread2.
2016-9-28 15:13:01: Thread2 dosoming1 ....
2016-9-28 15:13:01: الموضوع 1 نهاية.
2016-9-28 15:13:06: Thread2 dosoming2 ....
2016-9-28 15:13:06: تم الانتهاء من Thread2.
2016-9-28 15:13:06: تم تفريغ Thread2.
2016-9-28 15:13:06: Thread 2 End.
يمكن ملاحظة أن Thread1 يحصل على القفل أولاً ، وسيحصل Thread2 أيضًا على القفل لاحقًا ، لكن Thread1 قد شغله في هذا الوقت ، لذلك لا يحصل Thread2 على القفل حتى يقوم Thread1 بإطلاق القفل.
** يوضح هذا الرمز أن الخيط وراء LockInterruptly لاكتساب القفل يحتاج إلى الانتظار حتى يتم إصدار القفل السابق قبل الحصول على القفل. ** ولكن لا توجد ميزة مقاطعة حتى الآن ، لذلك تتم إضافة بعض التعليمات البرمجية إلى هذا:
thread2.start () ؛ حاول {thread.sleep (1000) ؛ } catch (interruptedException e) {E.PrintStackTrace () ؛} // interrupt thread2 في 1 second thread2.interrupt () ؛بعد بدء تشغيل Thread2 ، طريقة مقاطعة Call Thread2. حسنًا ، قم بتشغيل الرمز أولاً وشاهد النتائج:
2016-9-28 15:16:46: Thread 1 Get Lock.
2016-9-28 15:16:46: تم تأمين Thread1.
2016-9-28 15:16:46: Thread1 dosoming1 ....
2016-9-28 15:16:46: Thread 2 Get Lock.
2016-9-28 15:16:47: تم مقاطعة الموضوع 2. <-الاستجابة مباشرة لمقاطعة الموضوع
2016-9-28 15:16:51: Thread1 dosoming2 ....
2016-9-28 15:16:51: تم الانتهاء من Thread1.
2016-9-28 15:16:51: تم تفريغ Thread1.
2016-9-28 15:16:51: الموضوع 1 نهاية.
بالمقارنة مع الكود السابق ، يمكن العثور على أن Thread2 ينتظر إصدار Thread1 لإصدار القفل ، لكن Thread2 نفسه يقطع ، ولن يتم تنفيذ الكود وراء Thread2.
ReadWritelock
كما يوحي الاسم ، فهو قفل القراءة. يمكن فهم هذا النوع من سيناريو تطبيق قفل القراءة في القراءة بهذه الطريقة. على سبيل المثال ، يتم توفير موجة من البيانات في الغالب للقراءة ، ولا يوجد سوى عدد صغير نسبيًا من عمليات الكتابة. إذا تم استخدام قفل Mutex ، فسيؤدي ذلك إلى قفل المنافسة بين المواضيع. إذا كان بإمكان الجميع قراءتها عند القراءة ، قم بإغلاق مورد بمجرد كتابته. مثل هذه التغييرات تحل هذه المشكلة بشكل جيد ، مما يسمح لعملية القراءة بتحسين أداء القراءة دون التأثير على عملية الكتابة.
يمكن الوصول إلى مورد من قبل العديد من القراء ، أو الوصول إليه من قبل كاتب واحد ، ولا يمكن تنفيذ كلاهما في وقت واحد.
هذه هي الواجهة المجردة للقراءة والكتابة الأقفال ، وتحديد قفل القراءة وقفل الكتابة.
الواجهة العامة readWritelock { /*** إرجاع القفل المستخدم للقراءة. * * return القفل المستخدم لقراءة */ lock readlock () ؛ /*** إرجاع القفل المستخدم للكتابة. * * @Return القفل المستخدم للكتابة */ lock writelock () ؛}هناك تطبيق ReentranTreadWritelock في JDK ، وهو قفل إعادة القراءة. يمكن بناء reentrantreadwritelock في نوعين: عادلة أو غير عادلة. إذا لم يتم تحديدها بشكل صريح أثناء البناء ، فسيتم إنشاء قفل غير متطابق افتراضيًا. في وضع القفل غير القديم ، يكون ترتيب الوصول إلى مؤشرات الترابط غير مؤكد ، أي أنه يمكن اقتحامه ؛ يمكن تخفيفه من الكاتب إلى القارئ ، ولكن لا يمكن ترقية القارئ إلى الكاتب.
إذا كان وضع قفل عادل ، يتم تسليم الخيار إلى الخيط مع أطول وقت انتظار. إذا حصل مؤشر ترابط القراءة على القفل وطلب مؤشر ترابط الكتابة قفل الكتابة ، فلن يتم استلام عملية الاستحواذ على قفل القراءة حتى يتم اكتمال عملية الكتابة.
يحافظ تحليل الكود البسيط في الواقع على قفل المزامنة في reentrantreadwritelock ، لكنه يبدو دلالة مثل قفل القراءة وقفل الكتابة. ألق نظرة على مُنشئه:
REENTRANTREADWRITELOCK (معرض منطقي) {sync = عادلة؟ New Fairsync (): new nonfairsync () ؛ readerlock = جديد readlock (هذا) ؛ Horplock = new writelock (this) ؛} // مُنشئ قفل القراءة المحمي readlock (reentrantreadwritelock lock) {sync = lock.sync ؛} // مُنشئ القفل للكتابة المحمية (reentrantreadwritelock lock) {sync = lock.sync ؛}يمكنك أن ترى أن كائن قفل المزامنة لـ reentrantreadwritelock يتم الرجوع إليه فعليًا عند بنائه. وهذه فئة المزامنة هي فئة داخلية من reentrantreadwritelock. باختصار ، يتم إجراء أقفال قراءة/الكتابة من خلال المزامنة. كيف تتعاون على العلاقة بين الاثنين؟
// طريقة القفل لقراءة قفل الفراغ العام () {sync.acquireshared (1) ؛} // طريقة القفل لكتابة قفل قفل الفراغ العام () {sync.acquire (1) ؛}الفرق الرئيسي هو أن قفل القراءة يحصل على قفل مشترك ، بينما يكتسب قفل الكتابة قفلًا حصريًا. هناك نقطة يمكن ذكرها هنا ، أي لضمان إعادة reentrantreadwritelock ، يجب أن تدعم كل من الأقفال المشتركة والأقفال الحصرية تعدادًا وإعادة إدخال. يتم تخزين REENTRANTLOCK باستخدام الحالة ، ويمكن للدولة تخزين قيمة تشكيل واحدة فقط. لكي تكون متوافقة مع مشكلة قفلتين ، يتم تقسيمها إلى عدد مؤشرات الترابط التي تحمل القفل المشترك أو عدد مؤشرات الترابط التي تحمل القفل الحصري أو عدد إعادة الدخول على التوالي.
آخر
كتبت مقالة كبيرة شعرت أن الأمر سيستغرق وقتًا طويلاً للكتابة ، وهناك بعض الأقفال الأكثر فائدة:
العد التنازلي
هو وضع عداد يتم عقده في وقت واحد. عندما يستدعي المتصل طريقة انتظار CountDownlatch ، سيتم حظره إذا لم يكن العداد الحالي 0. يمكن أن يؤدي استدعاء طريقة الإصدار لـ CountDownlatch إلى تقليل العدد حتى يتم إلغاء حظر المتصل الذي يتصل به.
إشارة
Semaphore هو شكل من أشكال التفويض والترخيص ، مثل إنشاء 100 تراخيص ، بحيث يمكن لـ 100 سلسلة خيوط الاحتفاظ بأقفال في نفس الوقت ، وإذا تجاوز هذا المبلغ ، فسيعود إلى الفشل.
شكرًا لك على قراءة هذا المقال ، آمل أن يساعدك. شكرا لك على دعمك لهذا الموقع!