الجميع على دراية بنموذج Singleton ، وهم جميعًا يعرفون لماذا هو كسول ، جائع ، وما إلى ذلك ، لكن هل لديك فهم شامل لنمط Singleton؟ اليوم سوف آخذك لرؤية المفردات في عيني ، والتي قد تكون مختلفة عن فهمك.
هنا مثال بسيط بسيط:
. // طريقة البناء الخاصة للتأكد من أنه لا يمكن إنشاء فئات خارجية من خلال منشئ Singleton () {} // الحصول على مثيل Singleton Singleton GetInstance () {if (مثيل == null) {مثيل = New Singleton () ؛ } system.out.println ("أنا singleton كسول بسيط!") ؛ مثيل العودة ؛ }}من السهل أن نرى أن الكود أعلاه غير آمن في حالة متعدد الخيوط. عند إدخال موضوعين إذا (مثيل == فارغ) ، يحكم كلا الموضوعين على أن المثيل فارغ ، ثم يتم الحصول على حالتين. هذا ليس المفرد الذي نريده.
بعد ذلك ، نستخدم القفل لتحقيق الاستبعاد المتبادل لضمان تنفيذ المفردات.
. // طريقة البناء الخاصة للتأكد من أنه لا يمكن إنشاء فئات خارجية من خلال منشئ Singleton الخاص () {} // الحصول على مثيل Singleton Singleton Singleton GetInstance () {if (مثيل == null) {مثيل = New Singleton () ؛ } system.out.println ("أنا طريقة متزامنة كسول singleton!") ؛ مثيل العودة ؛ }}إضافة المزامنة هل تضمن سلامة الخيط ، ولكن هل هذه هي أفضل طريقة؟ من الواضح أنه ليس كذلك ، لأنه بهذه الطريقة ، في كل مرة نسميها طريقة GetInstance () ، سنكون مغلقين ، ونحن بحاجة فقط إلى قفلها عندما نسمي GetInstance () في المرة الأولى. من الواضح أن هذا يؤثر على أداء برنامجنا. نواصل إيجاد طرق أفضل.
بعد التحليل ، تبين أنه فقط من خلال ضمان أن مثيل = New Singleton () هو استبعاد المتبادل الخيط ، يمكن ضمان سلامة مؤشر الترابط ، وبالتالي فإن الإصدار التالي متاح:
// Double Lock Lazy Public Class Singleton {// Singleton مثيل متغير خاص ثابت ثابت Singleton = null ؛ // طريقة البناء الخاصة للتأكد من عدم تأكيد الفصول الخارجية من خلال مُنشئ المُنشئ () {} // الحصول على مثيل Singleton Singleton GetInstance () {if (مثيل == null) {Synchronized (singleton.class) {if (extal == null) {exatey = new singleton () ؛ }}} system.out.println ("أنا قفل مزدوج Lazy Singleton!") ؛ مثيل العودة ؛ }} هذه المرة يبدو أنه لا يحل فقط مشكلة سلامة مؤشرات الترابط ، ولكن لا يتسبب في إضافة قفل في كل مرة تتصل فيها GetInstance () ، مما يؤدي إلى تدهور الأداء. يبدو وكأنه حل مثالي ، هل هو في الواقع بهذه الطريقة؟
لسوء الحظ ، فإن الحقيقة ليست مثالية كما اعتقدنا. هناك آلية تسمى "كتابة المكتبات خارج الترتيب" في نموذج ذاكرة منصة Java. هذه الآلية هي التي تسبب فشل طريقة قفل الفحص المزدوج. يكمن مفتاح هذه المشكلة في السطر 5 على الكود أعلاه: مثيل = جديد Singleton () ؛ هذا الخط يفعل في الواقع شيئين: 1. اتصل بالمشارك وإنشاء مثيل. 2. قم بتعيين هذا المثيل إلى مثيل متغير المثيل. لكن المشكلة هي أن هاتين الخطوتين من JVM لا تضمن الأمر. وهذا يعني. قد يكون مثيله قد تم ضبطه على غير فارغ قبل استدعاء المُنشئ. دعنا نحللها معًا:
افترض أن هناك خيطان A و B
1. الموضوع A يدخل طريقة getInstance ().
2. لأن المثيل فارغ في هذا الوقت ، يدخل سلسلة A إلى الكتلة المتزامنة.
3. موضوع A ينفذ مثيل = New Singleton () ؛ يعين مثيل متغير المثيل إلى غير فارغ. (لاحظ أنه قبل استدعاء المنشئ.)
4. الموضوع A الخروج والموضوع B يدخل.
5. الخيط B يتحقق ما إذا كان المثيل فارغًا ، وليس فارغًا في هذا الوقت (في الخطوة الثالثة ، يتم تعيينه على غير فارغ حسب الموضوع أ). الموضوع ب إرجاع إشارة إلى مثيل. (تنشأ المشكلة. في هذا الوقت ، فإن الإشارة إلى المثيل ليست مثيلًا للفرد لأن المنشئ لا يسمى.)
6. خيط B يخرج وخيط A يدخل.
7. مؤشر الترابط A يستمر في استدعاء طريقة المنشئ ، ويكمل تهيئة المثيل ، والعودة.
أليس هناك طريقة جيدة؟ يجب أن تكون هناك طريقة جيدة ، دعنا نستمر في الاستكشاف!
. // طريقة البناء الخاصة للتأكد من أنه لا يمكن إنشاء فئات خارجية من خلال مُنشئ Singleton () {} // الحصول على مثيل Singleton Singleton GetInstance () {if (مثيل == null) {Synchronized (singleton.class) {// 1 singleton temp = exture ؛ // 2 if (temp == null) {synchronized (singleton.class) {// 3 temp = new singleton () ؛ // 4} مثيل = temp ؛ // 5}}} system.out.println ("أنا أحل الكتابة غير المطلوبة من singletons الكسول!") ؛ مثيل العودة ؛ }} 1. الموضوع A يدخل طريقة getInstance ().
2. لأن المثيل فارغ ، فإن سلسلة الرسائل A يدخل أول كتلة متزامنة في الموضع // 1.
3. مؤشر ترابط A ينفذ الرمز في الموضع // 2 ويعين مثيل إلى درجة حرارة المتغير المحلي. مثيل فارغ ، لذا فإن درجة الحرارة فارغة أيضًا.
4. لأن درجة الحرارة فارغة ، فإن مؤشر الترابط A يدخل الكتلة المتزامنة الثانية في الموضع /3. (اعتقدت أن هذا القفل كان زائدا بعض الشيء)
5. مؤشر ترابط A ينفذ الرمز في الموضع /4 ، ويُحدد درجة الحرارة إلى غير فارغ ، ولكن لم يتم استدعاء المُنشئ بعد! (مشكلة "Unorder Writing")
6. إذا كان الخيط A الكتل ، فإن الموضوع B يدخل طريقة getInstance ().
7. لأن المثيل فارغ ، يحاول الموضوع B إدخال الكتلة المتزامنة الأولى. ولكن لأن الموضوع A موجود بالفعل في الداخل. لذلك من المستحيل الدخول. الموضوع ب كتل.
8. تم تنشيط الموضوع A واستمر في تنفيذ الكود في الموضع /4. استدعاء المُنشئ. إنشاء مثيل.
9. تعيين مرجع مثيل لدرجة الحرارة إلى مثيل. الخروج من كتلتين متزامنين. إرجاع المثيل.
10. يتم تنشيط الخيط B ويدخل الكتلة المتزامنة الأولى.
11. مؤشر الترابط B ينفذ الرمز في الموضع // 2 ويعين مثيل المثيل إلى المتغير المحلي Temp.
12. يحدد الخيط B أن درجة الحرارة المتغيرة المحلية ليست فارغة ، لذلك يتخطى الكتلة if. إرجاع مثيل مثيل.
حتى الآن ، قمنا بحل المشكلة أعلاه ، لكننا وجدنا فجأة أنه من أجل حل مشكلة سلامة الخيط ، يبدو أن هناك الكثير من الخيوط ملفوفة على الجسم ... إنها فوضوية ، لذلك نحتاج إلى تبسيطها:
// Hungry Public Class Singleton {// Singleton ، يتم تهيئة متغير ثابت مرة واحدة عند تحميل الفصل لضمان مثيل Singleton الخاص بسلامة الخيوط الخاصة = New Singleton () ؛ // طريقة البناء الخاصة للتأكد من أنه لا يمكن إنشاء فئات خارجية من خلال المنشئ. خاص singleton () {} // احصل على مثيل كائن Singleton Singleton Singleton getInstance () {system.out.println ("أنا مفردة جائعة!") ؛ مثيل العودة ؛ }}عندما رأيت الكود أعلاه ، شعرت على الفور أن العالم كان هادئًا. ومع ذلك ، فإن هذه الطريقة تتبنى الطريقة الجائعة على غرار الرجل ، والتي تتمثل في كائنات سينجلتون مسبقة النهر. أحد عيب هذا هو: إذا كان المفرد في البناء كبيرًا ولا يتم استخدامه بعد اكتمال البناء ، فسيؤدي ذلك إلى إهدار الموارد.
هل هناك طريقة مثالية؟ استمر في المشاهدة:
// الفئة الداخلية تنفذ الفئة العامة البطيئة singleton {private static class singletonholder {// singleton fariable private static singleton extreal = new Singleton () ؛ } // طريقة البناء الخاصة لضمان عدم إنشاء فئات خارجية من خلال المنشئ. Singleton الخاص () {} // الحصول على كائن Singleton مثيل Singleton Singleton getInstance () {system.out.println ("أنا من الدرجة الداخلية!") ؛ إرجاع Singletonholder.instance ؛ }}كسول (تجنب نفايات الموارد أعلاه) ، آمن الخيط ، والرمز البسيط. نظرًا لأن آلية Java تنص على أن حامل Singletonholder الداخلي لن يتم تحميله إلا عند استدعاء طريقة getInstance () لأول مرة (تطبيق كسول) ، وعملية التحميل الخاصة بها آمنة من مؤشرات الترابط (تنفيذ مؤشر ترابط آمن). يتم إنشاء مثيل عند تحميل الفئة الداخلية.
دعنا نتحدث بإيجاز عن الكتابة غير المرتبة المذكورة أعلاه. هذه هي سمة JVM. على سبيل المثال ، إعلان اثنين من المتغيرين ، السلسلة أ ؛ السلسلة ب ؛ قد يحمل JVM أولاً أو ب. وبالمثل ، مثيل = New Singleton () ؛ قد تعيين مثيل على غير فارغ قبل استدعاء مُنشئ سينجلتون. هذا سؤال لكثير من الناس ، قائلاً إن كائنًا من Singleton لم يتم إنشاء مثيل له ، فكيف أصبحت الحالة غير فارغة؟ ما هي قيمتها الآن؟ إذا كنت تريد أن تفهم هذه المشكلة ، فيجب أن تفهم كيف مثيل الجملة = New Singleton () ؛ يتم تنفيذها. فيما يلي رمز زائف لشرحه لك:
MEM = تخصيص () ؛ // تخصيص الذاكرة لكائنات Singleton. مثيل = MEM ؛ // لاحظ أن المثيل غير فارغ الآن ، ولكن لم يتم تهيئته بعد. ctorsingleton (مثيل) ؛ // استدعاء مُنشئ المفرد والتمريرة.
يمكن ملاحظة أنه عندما ينفذ مؤشر ترابط مثيل = mem ؛ ، المثيل غير فارغ. إذا دخل مؤشر ترابط آخر إلى مثال البرنامج والقضاة باعتباره غير فارغ ، فسيقفز إلى مثال العودة ؛ وفي هذا الوقت ، لم يسمى مُنشئ سينجلتون المثيل ، والقيمة الحالية هي كائن الذاكرة الذي يتم إرجاعه بواسطة تخصيص () ؛. لذلك لا يحصل الخيط الثاني على كائن Singleton ، ولكن كائن ذاكرة.
ما سبق هو أفكاري الصغيرة وفهمي لنموذج المفرد. أرحب بحرارة بجميع الآلهة العظيمة القادمة وتوجيه وانتقاد.