دعونا أولاً نقرأ شرحًا مفصلاً للمزامنة:
المزامنة هي كلمة رئيسية في لغة جافا. عند استخدامه لتعديل طريقة أو كتلة رمز ، يمكنه التأكد من أن سلسلة رسائل واحدة تنفذ الرمز في نفس الوقت.
1. عندما يصل اثنين من مؤشرات الترابط المتزامنة هذه كتلة الكود المتزامن (هذا) في نفس كائن الكائن ، يمكن تنفيذ مؤشر ترابط واحد فقط خلال فترة واحدة. يجب أن ينتظر مؤشر ترابط آخر حتى يقوم مؤشر الترابط الحالي بتنفيذ كتلة الرمز هذه قبل تنفيذ كتلة الكود.
2. ومع ذلك ، عندما يصل مؤشر ترابط واحد إلى كتلة رمز المزامنة (هذا) متزامن للكائن ، لا يزال بإمكان مؤشر ترابط آخر الوصول إلى كتلة رمز التزامن غير المتزامنة (هذا) في هذا الكائن.
3. من الأهمية بمكان أنه عندما يصل مؤشر ترابط إلى كتلة رمز المزامنة (هذا) متزامن للكائن ، سيتم حظر مؤشرات الترابط الأخرى من الوصول إلى جميع كتل رمز التزامن المتزامنة (هذا) في الكائن.
4. ينطبق المثال الثالث أيضًا على كتل التعليمات البرمجية المتزامنة الأخرى. أي عندما يصل مؤشر ترابط إلى كتلة رمز المزامنة (هذا) متزامن لكائن ، فإنه يحصل على قفل كائن هذا الكائن. نتيجة لذلك ، يتم حظر مؤشرات الترابط الأخرى إلى جميع أجزاء الكود المتزامن لكائن الكائن مؤقتًا.
5. تنطبق القواعد أعلاه أيضًا على أقفال الكائنات الأخرى.
ببساطة ، يعلن المزامنة قفلًا للخيط الحالي. يمكن للمعلومات التي تحتوي على هذا القفل تنفيذ التعليمات في الكتلة ، ويمكن للمواضيع الأخرى الانتظار فقط حتى يتم الحصول عليه من القفل قبل نفس العملية.
هذا مفيد للغاية ، لكنني واجهت موقفًا غريبًا آخر.
1. في نفس الفئة ، هناك طريقتان: استخدام إعلان الكلمات الرئيسية المتزامنة
2. عند تنفيذ إحدى الطرق ، تحتاج إلى انتظار تنفيذ الطريقة الأخرى (رد الاتصال غير المتزامن) ، بحيث يمكنك استخدام خطوة العد للانتظار.
3. تم تفكيك الرمز على النحو التالي:
void synchronized a () {countDownLatch = new CountDownLatch (1) ؛ // القيام ببعض countdownlatch.await () ؛} void b () {countDownlatch.countdown () ؛} في
يتم تنفيذ الطريقة A بواسطة مؤشر الترابط الرئيسي ، يتم تنفيذ الطريقة B بواسطة مؤشر الترابط غير المتزامن ونتيجة تنفيذ رد الاتصال هي:
يبدأ الخيط الرئيسي في التعثر بعد تنفيذ الطريقة A ، ولم يعد يفعل ذلك ، وسيكون من غير المجدي أن تنتظر بغض النظر عن المدة التي يستغرقها.
هذه مشكلة قاتلة كلاسيكية
ينتظر تنفيذ B ، ولكن في الواقع ، لا تعتقد أن B هو رد اتصال ، B ينتظر أيضًا تنفيذ B. لماذا؟ يلعب متزامن دورًا.
بشكل عام ، عندما نريد مزامنة كتلة من الكود ، نحتاج إلى استخدام متغير مشترك لقفله ، على سبيل المثال:
byte [] mutex = new byte [0] ؛ void a1 () {synchronized (mutex) {// dosomething}} void b1 () {synchronized (mutex) {// dosomething}} إذا تم ترحيل محتويات الطريقة A وطريقة B إلى الكتل المتزامنة لطرق A1 و B1 على التوالي ، فسيكون من السهل فهمه.
بعد تنفيذ A1 ، سوف ينتظر بشكل غير مباشر طريقة (CountDownLatch) B1 للتنفيذ.
ومع ذلك ، نظرًا لعدم إصدار Mutex في A1 ، نبدأ في انتظار B1. في هذا الوقت ، حتى لو كانت طريقة رد الاتصال غير المتزامن B1 تحتاج إلى انتظار Mutex لإطلاق القفل ، فلن يتم تنفيذ طريقة B.
هذا تسبب في حالة خروج!
يتم وضع الكلمة الرئيسية المتزامنة هنا أمام الطريقة ، والوظيفة هي نفسها. إنه فقط أن لغة جافا تساعدك على إخفاء إعلان واستخدام Mutex. الطريقة المتزامنة المستخدمة في نفس الكائن هي نفسها ، لذلك حتى رد الاتصال غير المتزامن سيؤدي إلى حدوث طريق مسدود ، لذلك انتبه إلى هذه المشكلة. هذا المستوى من الخطأ هو أن الكلمة الرئيسية المتزامنة تستخدم بشكل غير صحيح. لا تستخدمه بشكل عشوائي ، واستخدمه بشكل صحيح.
إذن ما هو بالضبط مثل هذا كائن Mutex غير المرئي؟
المثال نفسه سهل التفكير. لأنه بهذه الطريقة ، ليست هناك حاجة لتحديد كائن جديد وصنع قفل. لإثبات هذه الفكرة ، يمكنك كتابة برنامج لإثبات ذلك.
الفكرة بسيطة للغاية. تحديد الفصل وهناك طريقتان. تم الإعلان عن مزامنة واحدة ، ويتم استخدام الآخر متزامن (هذا) في الجسم. ثم ابدأ موضوعين للاتصال هاتين الطريقتين بشكل منفصل. إذا حدثت منافسة القفل بين الطريقتين (الانتظار) ، فيمكن شرح أن الطفرات غير المرئية في المزامنة المعلنة بواسطة الطريقة هي في الواقع المثيل نفسه.
الطبقة العامة multiThReadsync {public synchronized void m1 () رميات interruptedException {system. Out.println ("M1 Call") ؛ خيط. النوم (2000) ؛ نظام. Out.println ("M1 Call Done") ؛ } public void m2 () يلقي interruptedException {synchronized (this) {system. Out.println ("M2 Call") ؛ خيط. النوم (2000) ؛ نظام. Out.println ("M2 Call Done") ؛ }} public static void main (string [] args) {Final MultiThreadsync thisobj = new MultIthReadsync () ؛ Thread T1 = New Thread () {Override public void run () {try {thisobj.m1 () ؛ } catch (interruptedException e) {E.PrintStackTrace () ؛ }}}} ؛ Thread T2 = New Thread () {Override public void run () {try {thisobj.m2 () ؛ } catch (interruptedException e) {E.PrintStackTrace () ؛ }}} ؛ t1.start () ؛ t2.start () ؛ }} الناتج الناتج هو:
M1 Callm1 Call Donem2 Callm2 Call
ويفضل أن كتلة المزامنة للطريقة M2 تنتظر تنفيذ M1. هذا يمكن أن يؤكد المفهوم أعلاه.
تجدر الإشارة إلى أنه عند إضافة المزامنة إلى الطريقة الثابتة ، نظرًا لأنها طريقة على مستوى الفصل ، فإن الكائن المقفل هو مثيل الفئة للفئة الحالية. يمكنك أيضًا كتابة برنامج لإثبات ذلك. هنا تم حذفه.
لذلك ، يمكن استبدال الكلمة الرئيسية المتزامنة بالطريقة تلقائيًا بـ (هذا) {} عند قراءتها ، وهو أمر سهل الفهم.
طريقة void () {void method method () {synchronized (this) {// biz code // biz code} ------ >>>}}} رؤية الذاكرة من المزامنة
في Java ، نعلم جميعًا أنه يمكن استخدام الكلمة الرئيسية المتزامنة لتنفيذ الاستبعاد المتبادل بين مؤشرات الترابط ، لكننا ننسى غالبًا أن لديها وظيفة أخرى ، أي ، لضمان رؤية المتغيرات في الذاكرة - أي عندما يتم قراءة مؤشر ترابط وتكتب الوصول إلى المتغير نفسه في نفس الوقت.
على سبيل المثال ، المثال التالي:
فئة الطبقة العامة ، {private static boolean جاهزة = false ؛ رقم int ثابت الخاص = 0 ؛ يمتد قارئ الفئة الثابتة الخاصة بتوسيع الموضوع {Override public void run () {بينما (! جاهز) {thread.yield () ؛ // دعم وحدة المعالجة المركزية للسماح لخيوط أخرى العمل} system.out.println (رقم) ؛ }} public static void main (string [] args) {new readerThread (). start () ؛ رقم = 42 ؛ جاهز = صحيح ؛ }}ما رأيك في قراءة المواضيع ستخرج؟ 42؟ في ظل الظروف العادية ، سيتم إخراج 42. ومع ذلك ، بسبب إعادة ترتيب المشكلات ، قد يخرج مؤشر ترابط القراءة 0 أو إخراج لا شيء.
نحن نعلم أنه يجوز للمترجم إعادة ترتيب الكود عند تجميع كود Java في Bytecode ، وقد يعيد وحدة المعالجة المركزية أيضًا ترتيب تعليماتها عند تنفيذ تعليمات الجهاز. طالما أن إعادة الترتيب لا تدمر دلالات البرنامج-
في سلسلة رسائل واحدة ، طالما أن إعادة الترتيب لا تؤثر على نتيجة تنفيذ البرنامج ، لا يمكن ضمان أن يتم تنفيذ العمليات الموجودة فيه بالترتيب المحدد في البرنامج ، حتى لو كان لإعادة الترتيب تأثير كبير على مؤشرات الترابط الأخرى.
هذا يعني أن تنفيذ البيان "جاهز = صحيح" قد يكون له الأسبقية على تنفيذ البيان "رقم = 42". في هذه الحالة ، قد يخرج مؤشر ترابط القراءة القيمة الافتراضية للرقم 0.
بموجب نموذج ذاكرة Java ، سيؤدي إعادة ترتيب المشكلات إلى مشاكل رؤية الذاكرة هذه. تحت نموذج ذاكرة Java ، يتمتع كل مؤشر ترابط ذاكرة العمل الخاصة به (بشكل أساسي ذاكرة التخزين المؤقت للوحدة المعالجة المركزية أو التسجيل) ، ويتم تنفيذ عملياته على المتغيرات في ذاكرته العاملة ، بينما يتم تحقيق التواصل بين المواضيع من خلال التزامن بين الذاكرة الرئيسية والذاكرة العاملة في مؤشر الترابط.
على سبيل المثال ، بالنسبة للمثال أعلاه ، قام مؤشر ترابط الكتابة بنجاح بتحديث الرقم إلى 42 وجاهزًا إلى True ، ولكن من المحتمل جدًا أن يقوم مؤشر ترابط الكتابة فقط بمزامنة الرقم إلى الذاكرة الرئيسية (ربما بسبب المخزن المؤقت للكتابة في وحدة المعالجة المركزية) ، مما يؤدي إلى قيام القيمة الجاهزة بقراءة مؤشرات ترابط القراءة اللاحقة دائمًا كاذبة ، وبالتالي فإن الكود أعلاه لن يخرج أي قيم رقمية.
إذا استخدمنا الكلمة الرئيسية المتزامنة للمزامنة ، فلن تكون هناك مشكلة من هذا القبيل.
فئة الطبقة العامة ، {private static boolean جاهزة = false ؛ رقم int ثابت الخاص = 0 ؛ قفل كائن ثابت خاص = كائن جديد () ؛ تمديد قارئ الفئة الثابتة الخاصة يمتد Thread {Override public void run () {synchronized (lock) {when (! ready) {thread.yield () ؛ } system.out.println (number) ؛ }} public static void main (string [] args) {synchronized (lock) {new readerThread (). start () ؛ رقم = 42 ؛ جاهز = صحيح ؛ }}} وذلك لأن نموذج ذاكرة Java يوفر الضمانات التالية للدلالات المتزامنة.
أي عندما تصدر Threada القفل M ، سيتم مزامنة المتغيرات التي كتبها (مثل X و Y ، والتي توجد في ذاكرتها العاملة) مع الذاكرة الرئيسية. عندما ينطبق ThreadB على نفس القفل M ، سيتم تعيين ذاكرة العمل الخاصة بـ ThreadB على غير صالح ، ثم يقوم ThreadB بإعادة تحميل المتغير الذي يريد الوصول إليه من الذاكرة الرئيسية إلى ذاكرته العاملة (في هذا الوقت ، x = 1 ، y = 1 ، هو أحدث قيمة تم تعديلها في Threada). وبهذه الطريقة ، يتم تحقيق التواصل بين المواضيع من threada إلى threadb.
هذا هو في الواقع واحدة من القواعد التي تحدث قبل JSR133. يحدد JSR133 المجموعة التالية من القواعد التي تحدث قبل نموذج ذاكرة Java.
في الواقع ، تحدد هذه المجموعة من القواعد التي تحدث قبل رؤية الذاكرة بين العمليات. إذا كانت A-select-be-be-be-be-be-be-be-be ، يجب أن تكون نتيجة التنفيذ لعملية ما (مثل الكتابة إلى المتغيرات) مرئية عند إجراء عملية B.
لاكتساب فهم أعمق لهذه القواعد التي تحدث ، دعونا نأخذ مثالاً:
// رمز مشترك بواسطة مؤشر الترابط A و B كائن = كائن جديد () ؛ int a = 0 ؛ int b = 0 ؛ int c = 0 ؛ // thread a ، اتصل بالشفرة التالية متزامنة (قفل) {a = 1 ؛ // 1 b = 2 ؛ // 2} // 3C = 3 ؛ // 4 // Thread B ، اتصل بالشفرة التالية متزامنة (قفل) {// 5 system.out.println (a) ؛ // 6 system.out.println (b) ؛ // 7 system.out.println (c) ؛ // 8}نحن نفترض أن مؤشر الترابط A يعمل أولاً ، ويقوم بتعيين القيم للمتغيرات الثلاثة A و B و C على التوالي (ملاحظة: يتم تنفيذ تعيين المتغيرات A ، B في كتلة البيان المتزامن) ، ثم يعمل مؤشر الترابط B مرة أخرى ، وقراءة قيم هذه المتغيرات الثلاثة وطبعتها. إذن ما هي قيم المتغيرات A و B و C المطبوعة بواسطة الموضوع B؟
وفقًا لقاعدة الخيوط الواحدة ، في تنفيذ الموضوع A ، يمكننا الحصول على أن عملية واحدة تحدث قبل عمليتين ، ويحدث 2 عملية قبل 3 عمليات ، ويحدث 3 عمليات قبل 4 عمليات. وبالمثل ، في تنفيذ الموضوع B ، تحدث 5 عمليات قبل 6 عمليات ، و 6 عمليات تحدث قبل 7 عمليات ، و 7 عمليات تحدث قبل 8 عمليات. وفقًا لمبادئ فتح الشاشة وقفلها ، تحدث العمليات الثلاثة (عملية فتح) قبل 5 عمليات (تشغيل القفل). وفقًا للقواعد المتعدية ، يمكننا أن نستنتج أن العمليات 1 و 2 تحدث قبل العمليات 6 و 7 و 8.
وفقًا لدلالات الذاكرة الخاصة بـ Seven-Be-Be-For ، يجب أن تكون نتائج تنفيذ العمليات 1 و 2 مرئية للعمليات 6 و 7 و 8 ، لذلك في الموضوع B و A و B يجب أن تكون 1 و 2. للعمليات 4 والعملية 8 من المتغير ج. لا يمكننا استنتاج العملية 4 قبل العملية 8 وفقًا للقواعد الموجودة قبل القواعد. لذلك ، في الموضوع B ، قد لا يزال المتغير الذي تم الوصول إليه إلى C 0 ، وليس 3.