أهلاً! نواصل دراستنا للتعددية. اليوم سنتعرف على الكلمة
volatile
الأساسية والطريقة yield()
. هيا بنا نتعمق :)
الكلمة الأساسية المتقلبة
عند إنشاء تطبيقات متعددة الخيوط، يمكن أن نواجه مشكلتين خطيرتين. أولاً، عند تشغيل تطبيق متعدد الخيوط، يمكن لخيوط مختلفة تخزين قيم المتغيرات مؤقتًا (تحدثنا عن هذا بالفعل في الدرس المعنون "استخدام المتغير" ). يمكن أن يكون لديك موقف يقوم فيه أحد الخيوط بتغيير قيمة المتغير، لكن الخيط الثاني لا يرى التغيير، لأنه يعمل مع نسخته المخزنة مؤقتًا من المتغير. وبطبيعة الحال، يمكن أن تكون العواقب خطيرة. لنفترض أنه ليس مجرد متغير قديم، بل رصيد حسابك المصرفي، والذي يبدأ فجأة بالقفز بشكل عشوائي لأعلى ولأسفل :) لا يبدو هذا ممتعًا، أليس كذلك؟ ثانيًا، في Java، تكون عمليات القراءة والكتابة لجميع الأنواع البدائية، باستثناءlong
و double
، عمليات ذرية. حسناً، على سبيل المثال، إذا قمت بتغيير قيمة متغير int
في موضوع ما، وفي موضوع آخر قرأت قيمة المتغير، فإما ستحصل على قيمته القديمة أو القيمة الجديدة، أي القيمة التي نتجت عن التغيير في الموضوع 1. لا توجد "قيم متوسطة". ومع ذلك، هذا لا يعمل مع long
s و double
s. لماذا؟ بسبب الدعم عبر الأنظمة الأساسية. هل تتذكر أننا قلنا في مستويات البداية أن المبدأ التوجيهي لجافا هو "الكتابة مرة واحدة، والتشغيل في أي مكان"؟ وهذا يعني الدعم عبر الأنظمة الأساسية. بمعنى آخر، يعمل تطبيق Java على جميع أنواع الأنظمة الأساسية المختلفة. على سبيل المثال، في أنظمة تشغيل Windows، إصدارات مختلفة من Linux أو MacOS. وسوف تعمل دون وجود عوائق على كل منهم. يبلغ وزنها 64 بت، long
وهي double
أثقل البدائيات في جافا. وبعض الأنظمة الأساسية 32 بت لا تنفذ ببساطة القراءة والكتابة الذرية لمتغيرات 64 بت. تتم قراءة هذه المتغيرات وكتابتها في عمليتين. أولاً، تتم كتابة أول 32 بت للمتغير، ثم تتم كتابة 32 بت أخرى. ونتيجة لذلك، قد تنشأ مشكلة. يكتب أحد الخيوط قيمة 64 بت إلى X
متغير ويقوم بذلك في عمليتين. في الوقت نفسه، يحاول الخيط الثاني قراءة قيمة المتغير ويفعل ذلك بين هاتين العمليتين - عندما تتم كتابة أول 32 بت، لكن الـ 32 بت الثانية لم تتم كتابتها. ونتيجة لذلك، فإنه يقرأ قيمة متوسطة وغير صحيحة، ويكون لدينا خطأ. على سبيل المثال، إذا حاولنا على مثل هذه المنصة كتابة الرقم إلى 9223372036854775809 في متغير، فسوف يشغل 64 بت. في النموذج الثنائي، يبدو كما يلي: 10000000000000000000000000000000000000000000000001 يبدأ الخيط الأول في كتابة الرقم للمتغير. في البداية، يكتب أول 32 بت (100000000000000000000000) ثم الـ 32 بت الثانية (000000000000000000000001). ويمكن ربط الخيط الثاني بين هذه العمليات، وقراءة القيمة المتوسطة للمتغير (1000000000000000000000000)، وهي أول 32 بت التي تمت كتابتها بالفعل. في النظام العشري، هذا الرقم هو 2,147,483,648. بمعنى آخر، أردنا فقط كتابة الرقم 9223372036854775809 إلى متغير، ولكن نظرًا لأن هذه العملية ليست ذرية على بعض المنصات، فلدينا الرقم الشرير 2,147,483,648، الذي خرج من العدم وسيكون له تأثير غير معروف برنامج. يقرأ الخيط الثاني ببساطة قيمة المتغير قبل الانتهاء من كتابته، أي أن الخيط رأى أول 32 بت، ولكن ليس الـ 32 بت الثانية. وبطبيعة الحال، لم تنشأ هذه المشاكل بالأمس. تحلها Java بكلمة رئيسية واحدة: volatile
. إذا استخدمنا volatile
الكلمة الأساسية عند الإعلان عن بعض المتغيرات في برنامجنا ...
public class Main {
public volatile long x = 2222222222222222222L;
public static void main(String[] args) {
}
}
…هذا يعني انه:
- سيتم قراءتها وكتابتها دائمًا بشكل ذري. حتى لو كان 64 بت
double
أوlong
. - لن يقوم جهاز Java بتخزينه مؤقتًا. لذلك لن يكون لديك موقف حيث تعمل 10 سلاسل رسائل مع نسخها المحلية الخاصة.
طريقة العائد ().
لقد قمنا بالفعل بمراجعة العديد منThread
أساليب الفصل الدراسي، ولكن هناك طريقة مهمة ستكون جديدة عليك. إنها yield()
الطريقة . ويفعل بالضبط ما يوحي به اسمه! عندما نستدعي yield
الطريقة على سلسلة رسائل، فإنها تتحدث في الواقع إلى الخيوط الأخرى: "مرحبًا يا شباب." أنا لست في عجلة من أمري للذهاب إلى أي مكان، لذا إذا كان من المهم لأي منكم أن يحصل على وقت المعالج، فليأخذه - يمكنني الانتظار". فيما يلي مثال بسيط لكيفية عمل ذلك:
public class ThreadExample extends Thread {
public ThreadExample() {
this.start();
}
public void run() {
System.out.println(Thread.currentThread().getName() + " yields its place to others");
Thread.yield();
System.out.println(Thread.currentThread().getName() + " has finished executing.");
}
public static void main(String[] args) {
new ThreadExample();
new ThreadExample();
new ThreadExample();
}
}
نقوم بإنشاء وبدء ثلاثة سلاسل بالتسلسل: Thread-0
و Thread-1
و و Thread-2
. Thread-0
يبدأ أولاً ويستسلم فوراً للآخرين. ثم Thread-1
يتم البدء وينتج أيضًا. ثم Thread-2
يتم البدء، والذي ينتج أيضًا. ليس لدينا أي سلاسل رسائل أخرى، وبعد أن Thread-2
حصل على مكانه الأخير، يقول برنامج جدولة سلاسل الرسائل، "حسنًا، ليس هناك المزيد من سلاسل الرسائل الجديدة." من لدينا في قائمة الانتظار؟ ومن الذي أخل بمكانته من قبل Thread-2
؟ ويبدو أنه كان Thread-1
. حسنًا، هذا يعني أننا سنتركه يعمل. Thread-1
يكمل عمله ثم يواصل برنامج جدولة الخيط تنسيقه: "حسنًا، Thread-1
انتهيت." هل لدينا أي شخص آخر في قائمة الانتظار؟ Thread-0 موجود في قائمة الانتظار: لقد حصل على مكانه قبل Thread-1
. والآن يحصل على دوره ويستمر حتى الاكتمال. ثم ينتهي المجدول من تنسيق سلاسل الرسائل: "حسنًا، Thread-2
لقد استسلمت لسلاسل الرسائل الأخرى، وقد تم الانتهاء منها جميعًا الآن." لقد كنت آخر من استسلم، والآن حان دورك. ثم Thread-2
يركض حتى الانتهاء. سيبدو إخراج وحدة التحكم كما يلي: يعطي Thread-0 مكانه للآخرين، يعطي Thread-1 مكانه للآخرين، يعطي Thread-2 مكانه للآخرين. انتهى تنفيذ Thread-1. انتهى تنفيذ مؤشر الترابط-0. تم الانتهاء من تنفيذ مؤشر الترابط 2. بالطبع، قد يبدأ برنامج جدولة سلاسل الرسائل بترتيب مختلف (على سبيل المثال، 2-1-0 بدلاً من 0-1-2)، لكن المبدأ يظل كما هو.
يحدث قبل القواعد
آخر شيء سنتطرق إليه اليوم هو مفهوم " يحدث من قبل ". كما تعلم بالفعل، في Java، يقوم برنامج جدولة سلاسل الرسائل بتنفيذ الجزء الأكبر من العمل المتضمن في تخصيص الوقت والموارد لسلاسل الرسائل لأداء مهامها. لقد رأيت أيضًا مرارًا وتكرارًا كيف يتم تنفيذ سلاسل الرسائل بترتيب عشوائي يكون من المستحيل عادةً التنبؤ به. وبشكل عام، بعد البرمجة "المتسلسلة" التي قمنا بها سابقًا، تبدو البرمجة متعددة الخيوط وكأنها شيء عشوائي. لقد أصبحت تعتقد بالفعل أنه يمكنك استخدام مجموعة من الأساليب للتحكم في تدفق برنامج متعدد مؤشرات الترابط. لكن تعدد مؤشرات الترابط في Java له ركيزة أخرى - القواعد الأربعة " يحدث قبل ". فهم هذه القواعد بسيط للغاية. تخيل أن لدينا خيطين -A
و B
. يمكن لكل من هذه الخيوط تنفيذ العمليات 1
و 2
. في كل قاعدة، عندما نقول ' A يحدث قبل B '، نعني أن جميع التغييرات التي أجراها الخيط A
قبل العملية 1
والتغييرات الناتجة عن هذه العملية تكون مرئية للخيط B
عند 2
تنفيذ العملية وبعدها. تضمن كل قاعدة أنه عند كتابة برنامج متعدد الخيوط، ستحدث أحداث معينة قبل الأحداث الأخرى بنسبة 100٪ من الوقت، وأنه في وقت التشغيل، سيكون 2
مؤشر الترابط B
دائمًا على علم بالتغييرات التي A
أجراها الخيط أثناء التشغيل 1
. دعونا نراجعها.
المادة 1.
يحدث تحرير كائن المزامنة (mutex) قبل أن يتم الحصول على نفس الشاشة بواسطة مؤشر ترابط آخر. أعتقد أنك تفهم كل شيء هنا. إذا تم الحصول على كائن المزامنة لكائن أو فئة بواسطة مؤشر ترابط واحد.، على سبيل المثال، بواسطة مؤشر ترابطA
، فلا يمكن لمؤشر ترابط آخر (مؤشر ترابط B
) الحصول عليه في نفس الوقت. يجب أن ينتظر حتى يتم تحرير كائن المزامنة (mutex).
القاعدة 2.
الطريقة تحدث منThread.start()
قبل . مرة أخرى، لا يوجد شيء صعب هنا. أنت تعلم بالفعل أنه لبدء تشغيل التعليمات البرمجية داخل الطريقة، يجب عليك استدعاء الطريقة في سلسلة المحادثات. على وجه التحديد، طريقة البداية، وليس الطريقة نفسها! تضمن هذه القاعدة أن قيم جميع المتغيرات التي تم تعيينها قبل الاستدعاء ستكون مرئية داخل الطريقة بمجرد البدء. Thread.run()
run()
start()
run()
Thread.start()
run()
القاعدة 3.
نهاية الطريقةrun()
تحدث قبل العودة من join()
الطريقة. لنعد إلى موضوعينا: A
و B
. نحن نسمي join()
الطريقة بحيث B
يتم ضمان انتظار الخيط حتى اكتمال الخيط A
قبل أن يقوم بعمله. هذا يعني أن طريقة الكائن A run()
مضمونة للتشغيل حتى النهاية. وجميع التغييرات التي تطرأ على البيانات التي تحدث في run()
طريقة الخيط A
مضمونة بنسبة مائة بالمائة أن تكون مرئية في الخيط B
بمجرد الانتهاء منها في انتظار A
انتهاء الخيط من عمله حتى يتمكن من بدء عمله الخاص.
القاعدة 4.
تتم الكتابة إلىvolatile
متغير قبل القراءة من نفس المتغير. عندما نستخدم الكلمة volatile
الأساسية، نحصل دائمًا على القيمة الحالية. حتى مع وجود long
أو double
(تحدثنا سابقًا عن المشكلات التي يمكن أن تحدث هنا). كما تعلم بالفعل، فإن التغييرات التي يتم إجراؤها على بعض سلاسل الرسائل لا تكون مرئية دائمًا لسلاسل الرسائل الأخرى. ولكن، بالطبع، هناك مواقف متكررة للغاية حيث لا يناسبنا هذا السلوك. لنفترض أننا قمنا بتعيين قيمة لمتغير في مؤشر الترابط A
:
int z;
….
z = 555;
إذا كان B
من المفترض أن يعرض مؤشر الترابط الخاص بنا قيمة المتغير z
على وحدة التحكم، فيمكنه بسهولة عرض 0، لأنه لا يعرف القيمة المخصصة. لكن القاعدة 4 تضمن أنه إذا أعلنا عن z
المتغير كـ volatile
، فإن التغييرات التي تطرأ على قيمته في مؤشر ترابط واحد ستكون دائمًا مرئية في مؤشر ترابط آخر. لو أضفنا الكلمة volatile
إلى الكود السابق...
volatile int z;
….
z = 555;
...ثم نمنع الموقف الذي B
قد يعرض فيه مؤشر الترابط 0. volatile
تتم الكتابة إلى المتغيرات قبل القراءة منها.
GO TO FULL VERSION