سلام! ما مطالعه خود را در مورد multithreading ادامه می دهیم.
وقتی متد را روی یک رشته صدا میزنیم
volatile
امروز با کلمه کلیدی و روش آن آشنا می شویم yield()
. بیا شیرجه بزنیم :)
کلمه کلیدی فرار
هنگام ایجاد برنامه های چند رشته ای، می توانیم با دو مشکل جدی مواجه شویم. اول، هنگامی که یک برنامه چند رشته ای در حال اجرا است، رشته های مختلف می توانند مقادیر متغیرها را در حافظه پنهان ذخیره کنند (ما قبلاً در این مورد در درس با عنوان 'استفاده از فرار' صحبت کردیم ). شما می توانید وضعیتی داشته باشید که یک رشته مقدار یک متغیر را تغییر می دهد، اما رشته دوم این تغییر را نمی بیند، زیرا با کپی حافظه پنهان متغیر خود کار می کند. به طور طبیعی، عواقب آن می تواند جدی باشد. فرض کنید این فقط یک متغیر قدیمی نیست، بلکه موجودی حساب بانکی شما است که ناگهان شروع به بالا و پایین پریدن می کند :) به نظر جالب نمی رسد، درست است؟ دوم، در جاوا، عملیات خواندن و نوشتن همه انواع اولیه، به جزlong
و double
، اتمی هستند. خوب، برای مثال، اگر مقدار یک int
متغیر را در یک رشته تغییر دهید، و در یک رشته دیگر، مقدار متغیر را بخوانید، یا مقدار قبلی آن را دریافت می کنید یا مقدار جدید، یعنی مقداری که از تغییر حاصل شده است. در موضوع 1. هیچ "مقادیر میانی" وجود ندارد. long
با این حال، این با s و double
s کار نمی کند . چرا؟ به دلیل پشتیبانی از پلتفرم های مختلف. به یاد داشته باشید که در سطوح ابتدایی گفتیم که اصل راهنمای جاوا این است که "یک بار بنویس، هر جا اجرا شود"؟ این به معنای پشتیبانی بین پلتفرمی است. به عبارت دیگر، یک برنامه جاوا بر روی انواع پلتفرم های مختلف اجرا می شود. به عنوان مثال، در سیستم عامل های ویندوز، نسخه های مختلف لینوکس یا MacOS. بدون هیچ مشکلی روی همه آنها اجرا خواهد شد. وزن آن 64 بیت است long
و double
«سنگینترین» اولیههای جاوا هستند. و برخی از سیستم عامل های 32 بیتی به سادگی خواندن و نوشتن اتمی متغیرهای 64 بیتی را پیاده سازی نمی کنند. چنین متغیرهایی در دو عملیات خوانده و نوشته می شوند. ابتدا 32 بیت اول روی متغیر نوشته می شود و سپس 32 بیت دیگر نوشته می شود. در نتیجه ممکن است مشکلی پیش بیاید. یک رشته مقداری 64 بیتی برای یک X
متغیر می نویسد و این کار را در دو عملیات انجام می دهد. در همان زمان، یک رشته دوم سعی می کند مقدار متغیر را بخواند و این کار را در بین این دو عملیات انجام می دهد - زمانی که 32 بیت اول نوشته شده است، اما 32 بیت دوم نوشته نشده است. در نتیجه یک مقدار متوسط و نادرست می خواند و ما باگ داریم. به عنوان مثال، اگر در چنین پلتفرمی بخواهیم عدد 9223372036854775809 را روی یک متغیر بنویسیم ، 64 بیت را اشغال می کند. به شکل باینری ، به نظر می رسد: 100000000000000000000000000000000000000000000000000000000001 اولین موضوع شروع به نوشتن شماره به متغیر می کند. در ابتدا 32 بیت اول (100000000000000000000000000000000000000000000000000000000000000000000000000000000000000) و سپس 32 بیت دوم (00000000000000000000000000000000001) و رشته دوم می تواند بین این عملیات فرو رود، با خواندن مقدار میانی متغیر (1000000000000000000000000000000000000000000000000000000000) که 32 بیت اولی است که قبلاً نوشته شده است. در سیستم اعشاری این عدد 2,147,483,648 است. به عبارت دیگر ما فقط می خواستیم عدد 9223372036854775809 را روی یک متغیر بنویسیم، اما به دلیل اتمی نبودن این عملیات در برخی از پلتفرم ها، عدد شیطانی 2,147,483,648 را داریم که از ناکجاآباد بیرون آمده و اثر نامعلومی خواهد داشت. برنامه رشته دوم به سادگی مقدار متغیر را قبل از پایان نوشتن می خواند، یعنی نخ 32 بیت اول را دید، اما 32 بیت دوم را نه. البته این مشکلات دیروز به وجود نیامد. جاوا آنها را با یک کلمه کلیدی حل می کند: volatile
. volatile
اگر از کلمه کلیدی هنگام اعلان یک متغیر در برنامه خود استفاده کنیم …
public class Main {
public volatile long x = 2222222222222222222L;
public static void main(String[] args) {
}
}
…این به آن معنا است:
- همیشه به صورت اتمی خوانده و نوشته خواهد شد. حتی اگر 64 بیتی
double
یاlong
. - ماشین جاوا آن را کش نمی کند. بنابراین موقعیتی نخواهید داشت که 10 رشته با نسخه های محلی خود کار کنند.
متد yield().
ما قبلاً بسیاری از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 اجرای خود را به پایان رسانده است. اجرای Thread-0 به پایان رسیده است. اجرای Thread-2 به پایان رسیده است. البته، زمانبندی رشته ممکن است رشتهها را به ترتیب دیگری شروع کند (مثلاً 2-1-0 به جای 0-1-2)، اما اصل یکسان است.
اتفاق می افتد-قبل از قوانین
آخرین چیزی که امروز به آن خواهیم پرداخت مفهوم " پیش از این اتفاق می افتد " است. همانطور که قبلاً می دانید، در جاوا زمانبندی thread بخش عمده ای از کار مربوط به تخصیص زمان و منابع به رشته ها را برای انجام وظایف خود انجام می دهد. همچنین بارها مشاهده کرده اید که چگونه رشته ها به ترتیب تصادفی اجرا می شوند که معمولاً پیش بینی آن غیرممکن است. و به طور کلی، پس از برنامه نویسی "متوالی" که قبلا انجام دادیم، برنامه نویسی چند رشته ای چیزی تصادفی به نظر می رسد. شما قبلاً به این باور رسیده اید که می توانید از روش های مختلفی برای کنترل جریان یک برنامه چند رشته ای استفاده کنید. اما multithreading در جاوا یک رکن دیگر دارد - قانون 4 " پیش از وقوع ". درک این قوانین بسیار ساده است. تصور کنید که ما دو رشته داریم -A
و B
. هر یک از این رشته ها می توانند عملیات 1
و 2
. در هر قانون، وقتی می گوییم " A اتفاق می افتد قبل از B "، منظور ما این است که تمام تغییرات ایجاد شده توسط thread A
قبل از عملیات 1
و تغییرات حاصل از این عملیات B
هنگام 2
انجام عملیات و پس از آن برای thread قابل مشاهده است. هر قانون تضمین می کند که وقتی یک برنامه چند رشته ای می نویسید، 100٪ مواقع رویدادهای خاصی قبل از دیگران اتفاق می افتد و در زمان عملیات 2
thread B
همیشه از تغییراتی که thread A
در طول عملیات ایجاد کرده است آگاه خواهد بود 1
. آنها را مرور کنیم.
قانون 1.
انتشار یک mutex قبل از اینکه همان مانیتور توسط نخ دیگری بدست آید اتفاق می افتد. فکر کنم اینجا همه چیزو میفهمی اگر mutex یک شی یا یک کلاس توسط یک رشته به دست بیاید. برای مثال، توسط نخA
، رشته دیگر (thread B
) نمی تواند آن را در همان زمان بدست آورد. باید صبر کرد تا mutex آزاد شود.
قانون 2.
روش قبلا اتفاق میThread.start()
افتد . باز هم اینجا هیچ چیز سختی نیست. از قبل می دانید که برای شروع اجرای کد داخل متد، باید متد موجود در thread را فراخوانی کنید. به طور مشخص، روش شروع، نه خود روش! این قانون تضمین می کند که مقادیر همه متغیرهای تنظیم شده قبل از فراخوانی در داخل متد پس از شروع قابل مشاهده خواهد بود. Thread.run()
run()
start()
run()
Thread.start()
run()
قانون 3.
پایان متدrun()
قبل از بازگشت از join()
متد اتفاق می افتد. بیایید به دو موضوع خود برگردیم: A
و B
. ما join()
متد را فراخوانی می کنیم تا thread تضمین شود که قبل از اینکه کار خود را انجام دهد B
منتظر تکمیل نخ بماند . A
این بدان معناست که متد شیء A run()
تا انتها اجرا می شود. و تمام تغییرات دادهها که در run()
روش thread اتفاق میافتد A
، صد در صد تضمین میشود که B
پس از اتمام کار در رشته قابل مشاهده هستند و منتظر میمانند تا Thread A
کار خود را تمام کند تا بتواند کار خود را شروع کند.
قانون 4.
نوشتن روی یکvolatile
متغیر قبل از خواندن از همان متغیر اتفاق می افتد. وقتی از کلمه کلیدی استفاده می کنیم volatile
، در واقع همیشه مقدار فعلی را دریافت می کنیم. حتی با یک long
یا double
(ما قبلاً در مورد مشکلاتی که در اینجا ممکن است رخ دهد صحبت کردیم). همانطور که قبلاً متوجه شدید، تغییرات ایجاد شده در برخی از رشته ها همیشه برای رشته های دیگر قابل مشاهده نیست. اما، البته، موقعیت های بسیار مکرری وجود دارد که چنین رفتاری برای ما مناسب نیست. فرض کنید به یک متغیر روی thread مقداری اختصاص می دهیم A
:
int z;
….
z = 555;
اگر B
موضوع ما باید مقدار z
متغیر را در کنسول نمایش دهد، به راحتی می تواند 0 را نمایش دهد، زیرا از مقدار اختصاص داده شده اطلاعی ندارد. z
اما قانون 4 تضمین می کند که اگر متغیر را به عنوان اعلان کنیم volatile
، تغییرات مقدار آن در یک رشته همیشه در رشته دیگر قابل مشاهده خواهد بود. volatile
اگر به کلمه کد قبلی اضافه کنیم ...
volatile int z;
….
z = 555;
...سپس از موقعیتی جلوگیری می کنیم که رشته B
ممکن است 0 را نشان دهد. نوشتن روی volatile
متغیرها قبل از خواندن از آنها اتفاق می افتد.
GO TO FULL VERSION