تنقسم المتغيرات في Java إلى فئتين: المتغيرات المحلية ومتغيرات الفئة. تشير المتغيرات المحلية إلى المتغيرات المحددة ضمن طريقة ما، مثل المتغيرات المحددة في طريقة التشغيل. بالنسبة لهذه المتغيرات، لا توجد مشكلة في المشاركة بين المواضيع. ولذلك، فإنها لا تتطلب مزامنة البيانات. متغيرات الفئة هي متغيرات محددة في فئة، ونطاقها هو الفئة بأكملها. يمكن مشاركة هذه المتغيرات بواسطة عدة سلاسل رسائل. ولذلك، نحن بحاجة إلى إجراء مزامنة البيانات على مثل هذه المتغيرات.
تعني مزامنة البيانات أن مؤشر ترابط واحد فقط يمكنه الوصول إلى متغيرات الفئة المتزامنة في نفس الوقت. بعد وصول مؤشر الترابط الحالي إلى هذه المتغيرات، يمكن لمؤشرات الترابط الأخرى الاستمرار في الوصول إليها. يشير الوصول المذكور هنا إلى الوصول من خلال عمليات الكتابة. إذا كانت جميع سلاسل الرسائل التي تصل إلى متغيرات الفئة عبارة عن عمليات قراءة، فلن تكون مزامنة البيانات مطلوبة بشكل عام. فماذا يحدث إذا لم يتم تنفيذ مزامنة البيانات على متغيرات الفئة المشتركة؟ دعونا نرى أولاً ما يحدث مع الكود التالي:
انسخ رمز الكود كما يلي:
اختبار الحزمة
الطبقة العامة MyThread تمتد الموضوع
{
عدد صحيح عام ثابت n = 0;
تشغيل الفراغ العام ()
{
كثافة العمليات م = ن؛
أَثْمَر()؛
م++;
ن = م؛
}
يلقي الفراغ الرئيسي العام (String[] args) استثناءً
{
MyThread myThread = new MyThread ();
Thread threads[] = new Thread[100];
لـ (int i = 0; i < Threads.length; i++)
Threads[i] = new Thread(myThread);
لـ (int i = 0; i < Threads.length; i++)
المواضيع[i].start();
لـ (int i = 0; i < Threads.length; i++)
المواضيع[i].join();
System.out.println("n = "+ MyThread.n);
}
}
النتائج المحتملة لتنفيذ الكود أعلاه هي كما يلي:
انسخ رمز الكود كما يلي:
ن = 59
قد يفاجأ العديد من القراء برؤية هذه النتيجة. من الواضح أن هذا البرنامج يبدأ 100 موضوع، ثم يقوم كل موضوع بزيادة المتغير الثابت n بمقدار 1. أخيرًا، استخدم طريقة الانضمام لتشغيل جميع سلاسل الرسائل المائة، ثم قم بإخراج القيمة n. عادة، يجب أن تكون النتيجة ن = 100. لكن النتيجة أقل من 100.
في الواقع، المذنب في هذه النتيجة هو "البيانات القذرة" التي نذكرها كثيرًا. بيان العائد () في طريقة التشغيل هو البادئ "للبيانات القذرة" (بدون إضافة عبارة العائد، يمكن أيضًا إنشاء "بيانات قذرة"، لكنها لن تكون واضحة جدًا. فقط عن طريق تغيير 100 إلى رقم أكبر سيتم غالبًا ما يحدث إنشاء "بيانات قذرة"، ويهدف استدعاء العائد في هذا المثال إلى تضخيم تأثير "البيانات القذرة"). وظيفة طريقة العائد هي إيقاف الخيط مؤقتًا، أي جعل الخيط الذي يستدعي طريقة العائد يتخلى مؤقتًا عن موارد وحدة المعالجة المركزية، مما يمنح وحدة المعالجة المركزية فرصة لتنفيذ سلاسل رسائل أخرى. لتوضيح كيفية إنشاء هذا البرنامج "بيانات قذرة"، لنفترض أنه تم إنشاء خيطين فقط: Thread1 وthread2. نظرًا لأن طريقة بدء Thread1 يتم استدعاؤها أولاً، فسيتم تشغيل طريقة تشغيل Thread1 أولاً بشكل عام. عندما يتم تشغيل طريقة تشغيل Thread1 إلى السطر الأول (int m = n;)، يتم تعيين قيمة n إلى m. عند تنفيذ طريقة العائد للسطر الثاني، سيتوقف مؤشر الترابط 1 عن التنفيذ مؤقتًا. عند إيقاف مؤشر الترابط 1 مؤقتًا، يبدأ تشغيل مؤشر الترابط 2 بعد الحصول على موارد وحدة المعالجة المركزية (كان مؤشر الترابط 2 في حالة الاستعداد من قبل). m = n;)، نظرًا لأن n لا يزال يساوي 0 عند تنفيذ Thread1 لتحقيق العائد، فإن القيمة التي تم الحصول عليها بواسطة m في Thread2 هي أيضًا 0. يؤدي هذا إلى حصول قيمتي m لـ thread1 و thread2 على 0. بعد تنفيذ طريقة العائد، يبدأون جميعًا من 0 ويضيفون 1. لذلك، بغض النظر عمن ينفذها أولاً، فإن القيمة النهائية لـ n هي 1، ولكن يتم تعيين قيمة لـ n بواسطة Thread1 وthread2 على التوالي. قد يتساءل شخص ما، إذا كان هناك n++ فقط، فهل سيتم إنشاء "بيانات قذرة"؟ الجواب هو نعم. لذا فإن n++ هو مجرد بيان، فكيف يتم تسليم وحدة المعالجة المركزية إلى سلاسل رسائل أخرى أثناء التنفيذ؟ في الواقع، هذه مجرد ظاهرة سطحية بعد أن يتم تجميع n++ إلى لغة وسيطة (وتسمى أيضًا bytecode) بواسطة مترجم Java، فهي ليست لغة. دعونا نرى ما هي لغة جافا الوسيطة التي سيتم تجميع كود جافا التالي إليها.
انسخ رمز الكود كما يلي:
تشغيل الفراغ العام ()
{
ن++;
}
رمز اللغة المتوسطة المترجمة
انسخ رمز الكود كما يلي:
تشغيل الفراغ العام ()
{
aload_0
مكرر
com.getfield
Iconst_1
iadd
putfield
يعود
}
يمكنك أن ترى أنه لا يوجد سوى عبارة n ++ في طريقة التشغيل، ولكن بعد التجميع، هناك 7 عبارات لغة متوسطة. لا نحتاج إلى معرفة وظائف هذه العبارات، فقط انظر إلى العبارات الموجودة في الأسطر 005 و007 و008. السطر 005 هو getfield حسب معناه باللغة الإنجليزية، ونحن نعلم أننا نريد الحصول على قيمة معينة لأنه يوجد n واحد فقط، فلا شك أننا نريد الحصول على قيمة n. ليس من الصعب تخمين أن iadd في السطر 007 هو إضافة 1 إلى قيمة n التي تم الحصول عليها. أعتقد أنك قد خمنت معنى putfield في السطر 008. وهو مسؤول عن تحديث n بعد إضافة 1 مرة أخرى إلى متغير الفئة n. بالحديث عن هذا، ربما لا يزال لديك شك عند تنفيذ n++، يكفي فقط إضافة n بمقدار 1. لماذا يستغرق الأمر الكثير من المتاعب؟ في الواقع، يتضمن هذا مشكلة في نموذج ذاكرة Java.
ينقسم نموذج ذاكرة Java إلى منطقة تخزين رئيسية ومنطقة تخزين عاملة. تقوم منطقة التخزين الرئيسية بتخزين جميع المثيلات في Java. وهذا يعني أنه بعد أن نستخدم الجديد لإنشاء كائن، يتم حفظ الكائن وأساليبه الداخلية ومتغيراته وما إلى ذلك في هذه المنطقة، ويتم حفظ n في فئة MyThread في هذه المنطقة. يمكن مشاركة وحدة التخزين الرئيسية بين كافة المواضيع. منطقة تخزين العمل هي مكدس الخيط الذي تحدثنا عنه سابقًا، حيث يتم تخزين المتغيرات المحددة في طريقة التشغيل والطريقة التي تستدعيها طريقة التشغيل، أي متغيرات الطريقة. عندما يريد مؤشر ترابط تعديل المتغيرات في منطقة التخزين الرئيسية، فإنه لا يقوم بتعديل هذه المتغيرات مباشرة، ولكن ينسخها إلى منطقة تخزين العمل الخاصة بمؤشر الترابط الحالي. بعد اكتمال التعديل، يتم استبدال قيمة المتغير بالمتغير المقابل القيمة في منطقة التخزين الرئيسية قيمة متغيرة.
بعد فهم نموذج ذاكرة Java، ليس من الصعب فهم سبب عدم اعتبار n++ عملية ذرية. يجب أن يمر بعملية النسخ وإضافة 1 والكتابة فوقه. تشبه هذه العملية تلك التي تمت محاكاتها في فئة MyThread. كما يمكنك أن تتخيل، إذا تمت مقاطعة مؤشر الترابط 1 لسبب ما عند تنفيذ getfield، فسيحدث موقف مشابه لنتيجة تنفيذ فئة MyThread. لحل هذه المشكلة تمامًا، يجب علينا استخدام بعض الطرق لمزامنة n، أي أن مؤشر ترابط واحد فقط يمكنه تشغيل n في نفس الوقت، وهو ما يسمى أيضًا العملية الذرية على n.