JavaRush /مدونة جافا /Random-AR /إدارة التدفق. الكلمة الأساسية المتقلبة وطريقة العائد ().

إدارة التدفق. الكلمة الأساسية المتقلبة وطريقة العائد ().

نشرت في المجموعة
مرحبًا! نواصل دراسة مؤشرات الترابط المتعددة، واليوم سنتعرف على كلمة رئيسية جديدة - المتقلبة وطريقة العائد (). دعونا معرفة ما هو :)

الكلمة الرئيسية متقلبة

عند إنشاء تطبيقات متعددة الخيوط، يمكن أن نواجه مشكلتين خطيرتين. أولاً، أثناء تشغيل تطبيق متعدد الخيوط، يمكن لخيوط مختلفة تخزين قيم المتغيرات مؤقتًا (سنتحدث أكثر عن هذا في محاضرة "استخدام المتطايرة" ). من الممكن أن يكون أحد الخيوط قد غيّر قيمة متغير، لكن الثاني لم يرى هذا التغيير لأنه كان يعمل مع نسخته المخزنة مؤقتًا من المتغير. وبطبيعة الحال، يمكن أن تكون العواقب خطيرة. تخيل أن هذا ليس مجرد نوع من "المتغير"، ولكن، على سبيل المثال، رصيد بطاقتك المصرفية، والذي بدأ فجأة في القفز بشكل عشوائي ذهابًا وإيابًا :) ليس لطيفًا جدًا، أليس كذلك؟ ثانيا، في جافا، عمليات القراءة والكتابة على جميع أنواع الحقول ما عدا longوهي doubleذرية. ما هي الذرية؟ حسنًا، على سبيل المثال، إذا قمت بتغيير قيمة متغير في موضوع واحد int، وفي موضوع آخر قرأت قيمة هذا المتغير، فستحصل إما على قيمته القديمة أو قيمة جديدة - تلك التي ظهرت بعد التغيير في الموضوع 1. ربما لن تظهر "خيارات وسيطة" هناك. ومع ذلك، هذا لا يعمل مع longو . doubleلماذا؟ لأنه عبر منصة. هل تتذكر كيف قلنا في المستويات الأولى أن مبدأ Java "مكتوب مرة واحدة، ويعمل في كل مكان"؟ هذا هو عبر منصة. أي أن تطبيق Java يعمل على منصات مختلفة تمامًا. على سبيل المثال، في أنظمة تشغيل Windows، إصدارات مختلفة من Linux أو MacOS، وفي كل مكان سيعمل هذا التطبيق بثبات. longو- doubleأثقل البدائيات في جافا: يبلغ وزنها 64 بت. وبعض الأنظمة الأساسية 32 بت لا تنفذ ببساطة ذرية قراءة وكتابة متغيرات 64 بت. تتم قراءة هذه المتغيرات وكتابتها في عمليتين. أولا، تتم كتابة أول 32 بت للمتغير، ثم 32 بت أخرى. وبناء على ذلك، في هذه الحالات قد تنشأ مشكلة. يكتب أحد الخيوط قيمة 64 بت إلى متغيرХ، وهو يفعل ذلك "في خطوتين". في الوقت نفسه، يحاول الخيط الثاني قراءة قيمة هذا المتغير، ويقوم بذلك بشكل صحيح في المنتصف، عندما تكون أول 32 بت قد تمت كتابتها بالفعل، لكن البتات الثانية لم تتم كتابتها بعد. ونتيجة لذلك، فإنه يقرأ قيمة متوسطة وغير صحيحة، ويحدث خطأ. على سبيل المثال، إذا حاولنا في مثل هذه المنصة كتابة رقم لمتغير - 9223372036854775809 - فسوف يشغل 64 بت. في الشكل الثنائي سيبدو كما يلي: 10000000000000000000000000000000000000000000000000001 سيبدأ الخيط الأول في كتابة هذا الرقم إلى متغير، وسيكتب أول 32 بت أولاً: 100000000000000000000000 00000 00000 ثم الثاني 32: 0000000000000000000000001 ويمكن أن يدخل الخيط الثاني في هذه الفجوة و اقرأ القيمة المتوسطة للمتغير - 10000000000000000000000000، أول 32 بت تمت كتابتها بالفعل. في النظام العشري، هذا الرقم يساوي 2147483648. أي أننا أردنا فقط كتابة الرقم 9223372036854775809 في متغير، ولكن نظرًا لأن هذه العملية على بعض المنصات ليست ذرية، فقد حصلنا على الرقم "الأيسر" 2147483648 الذي لا نحتاجه من العدم ومن غير المعروف كيف سيؤثر على عمل البرنامج. يقرأ الخيط الثاني ببساطة قيمة المتغير قبل كتابته أخيرًا، أي أنه رأى أول 32 بت، ولكن ليس الـ 32 بت الثانية. هذه المشكلات، بالطبع، لم تنشأ بالأمس، وفي Java يتم حلها باستخدام كلمة رئيسية واحدة فقط - volatile . إذا أعلنا عن بعض المتغيرات في برنامجنا بالكلمة volatile...
public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…هذا يعني انه:
  1. سيتم دائمًا قراءتها وكتابتها ذريًا. حتى لو كان 64 بت doubleأو long.
  2. لن يقوم جهاز Java بتخزينه مؤقتًا. لذلك يتم استبعاد الموقف عندما تعمل 10 سلاسل مع نسخها المحلية.
هكذا يتم حل مشكلتين خطيرتين للغاية في كلمة واحدة :)

طريقة العائد ().

لقد نظرنا بالفعل في العديد من أساليب الفصل الدراسي Thread، ولكن هناك طريقة واحدة مهمة ستكون جديدة بالنسبة لك. هذه هي طريقة العائد () . ترجمت من الإنجليزية على أنها "الاستسلام". وهذا بالضبط ما تفعله الطريقة! إدارة التدفق.  الكلمة الأساسية المتقلبة وطريقة العائد () - 2عندما نطلق على طريقة العائد على سلسلة رسائل، فإنها تقول في الواقع لسلاسل رسائل أخرى: "حسنًا يا رفاق، أنا لست في عجلة من أمري، لذا إذا كان من المهم لأي منكم الحصول على وقت لوحدة المعالجة المركزية، خذ هذا، أنا ليست عاجلة." فيما يلي مثال بسيط لكيفية عمله:
public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + "give way 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. والآن وصل الأمر إليه، وهو ينفذ إلى النهاية. بعد ذلك، ينتهي المجدول من تنسيق المواضيع: "حسنًا، الموضوع 2، لقد أفسحت المجال لخيوط أخرى، لقد عملت جميعها بالفعل. لقد كنت آخر من أعطى الأولوية، والآن حان دورك." بعد ذلك، يتم تشغيل Thread-2 حتى الاكتمال. ستبدو مخرجات وحدة التحكم كما يلي: مؤشر الترابط 0 يفسح المجال للآخرين مؤشر الترابط 1 يفسح المجال للآخرين مؤشر الترابط 2 يفسح المجال للآخرين انتهى تنفيذ مؤشر الترابط 1. انتهى تنفيذ مؤشر الترابط-0. تم الانتهاء من تنفيذ مؤشر الترابط 2. يمكن لمجدول سلاسل الرسائل، بالطبع، تشغيل سلاسل الرسائل بترتيب مختلف (على سبيل المثال، 2-1-0 بدلاً من 0-1-2)، لكن المبدأ هو نفسه.

يحدث قبل القواعد

آخر شيء سنتطرق إليه اليوم هو مبادئ " يحدث قبل ". كما تعلم بالفعل، في Java، يتم تنفيذ معظم أعمال تخصيص الوقت والموارد لسلاسل الرسائل لإكمال مهامها بواسطة برنامج جدولة سلاسل المحادثات. لقد رأيت أيضًا أكثر من مرة كيف يتم تنفيذ سلاسل الرسائل بترتيب تعسفي، وفي أغلب الأحيان يكون من المستحيل التنبؤ بذلك. وبشكل عام، بعد البرمجة "التسلسلية" التي قمنا بها من قبل، يبدو تعدد مؤشرات الترابط وكأنه شيء عشوائي. كما رأيت بالفعل، يمكن التحكم في تقدم برنامج متعدد الخيوط باستخدام مجموعة كاملة من الأساليب. ولكن بالإضافة إلى ذلك، يوجد في Java Multithreading "جزيرة استقرار" أخرى - 4 قواعد تسمى " يحدث من قبل ". يتم ترجمة هذا حرفيًا من اللغة الإنجليزية على أنه "يحدث من قبل" أو "يحدث من قبل". معنى هذه القواعد سهل الفهم. تخيل أن لدينا خيطين - Aو B. يمكن لكل من هذه الخيوط تنفيذ العمليات 1و 2. وعندما نقول في كل قاعدة من القواعد " A يحدث قبل B "، فهذا يعني أن جميع التغييرات التي أجراها الخيط Aقبل العملية 1والتغييرات التي تستلزمها هذه العملية تكون مرئية للخيط Bفي وقت تنفيذ العملية 2و بعد إجراء العملية. تضمن كل قاعدة من هذه القواعد أنه عند كتابة برنامج متعدد الخيوط، فإن بعض الأحداث ستحدث قبل أخرى بنسبة 100% من الوقت، وأن الخيط Bوقت العملية 2سيكون دائمًا على علم بالتغييرات التي Аأجراها الخيط أثناء العملية 1. دعونا ننظر إليهم.

المادة 1.

يحدث تحرير كائن المزامنة (mutex) قبل حدوثه قبل أن يحصل مؤشر ترابط آخر على نفس الشاشة. حسنًا، كل شيء يبدو واضحًا هنا. إذا تم الحصول على كائن المزامنة لكائن أو فئة بواسطة مؤشر ترابط واحد، على سبيل المثال، مؤشر ترابط А، فلا يمكن لمؤشر ترابط آخر (مؤشر ترابط B) الحصول عليه في نفس الوقت. عليك الانتظار حتى يتم تحرير كائن المزامنة (mutex).

القاعدة 2.

Thread.start() يحدث قبل الطريقة Thread.run(). لا شيء معقد أيضًا. أنت تعرف بالفعل: لكي يبدأ تنفيذ التعليمات البرمجية الموجودة داخل الطريقة run()، تحتاج إلى استدعاء الطريقة الموجودة على سلسلة الرسائل start(). إنه أسلوبه، وليس الأسلوب نفسه run()! تضمن هذه القاعدة أن Thread.start()تكون قيم جميع المتغيرات التي تم ضبطها قبل التنفيذ مرئية داخل الطريقة التي بدأت بالتنفيذ run().

القاعدة 3.

يحدث إكمال الطريقة run() قبل الخروج من الطريقة join(). دعونا نعود إلى تيارينا - Аو B. نحن نسمي الطريقة join()بحيث Bيجب على الخيط الانتظار حتى اكتماله Aقبل القيام بعمله. هذا يعني أن طريقة run()الكائن A ستعمل بالتأكيد حتى النهاية. وجميع التغييرات في البيانات التي تحدث في طريقة run()الخيط Aستكون مرئية بالكامل في الخيط Bعندما ينتظر اكتماله Aويبدأ العمل بنفسه.

القاعدة 4.

الكتابة إلى متغير متقلب تتم قبل القراءة من نفس المتغير. باستخدام الكلمة الأساسية المتقلبة، سنحصل دائمًا على القيمة الحالية. حتى في حالة longو double، المشاكل التي نوقشت في وقت سابق. كما تعلم بالفعل، فإن التغييرات التي يتم إجراؤها في بعض سلاسل الرسائل لا تكون مرئية دائمًا لسلاسل الرسائل الأخرى. ولكن، بالطبع، في كثير من الأحيان هناك مواقف عندما لا يناسبنا سلوك البرنامج هذا. لنفترض أننا قمنا بتعيين قيمة لمتغير في سلسلة رسائل A:
int z;.

z= 555;
إذا كان خيطنا Bسيطبع قيمة متغير zإلى وحدة التحكم، فيمكنه بسهولة طباعة 0 لأنه لا يعرف القيمة المخصصة له. لذلك، تضمن لنا القاعدة 4: إذا أعلنت أن المتغير zمتغير، فإن التغييرات التي تطرأ على قيمه في خيط واحد ستكون مرئية دائمًا في خيط آخر. إذا أضفنا الكلمة volatile إلى الكود السابق...
volatile int z;.

z= 555;
... يتم استبعاد الموقف الذي Bسيخرج فيه الدفق 0 إلى وحدة التحكم. تتم الكتابة إلى المتغيرات المتقلبة قبل القراءة منها.
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION