حسام درویشیان
حسام درویشیان
خواندن ۴ دقیقه·۳ سال پیش

دقیقا Volatile توی Java چیست؟

تو کامپیوتر CPU مسئول اجرای دستورات هستش و برای بازیابی دستورات و داده های به RAM نیاز داره. CPU توانایی زیادی در اجرای دستورات داره و سرعت دسترسی به داده های RAM مناسب با سرعت پردازش اونها نیست. به همین دلیل از یکسری تکنیک برای بهینه کردن فرآیند ها استفاده می‌کنه.


تکنیک‌ها:

تکنیک Out-of-order execution: یک از تکنیک های CPU برای بهینه کردن زمان اجرا برنامه هاست و بجای اینکه منتظر داده‌های دستور جاری بشه. دستورات بعدی را که قابلیت اجرا داره رو اجرا می‌کند.

تکنیک Branch predictor: یک مدار دیجیتال هستش برای پیش بینی بلاکی که قراره در دستورات شرطی اجرا بشه. در ابتدا شاید زیاد کار آمد به نظر نرسه ولی با گذشت زمان و نگهداری از تاریخچه و … احتمال انتخاب درست افزایش پیدا می‌کنه و در زمان اجرا صرفه جویی می‌کند.

تکنیک Speculative execution: یکی دیگر از تکنیک های بهینه سازی زمان در CPU هستش. این تکنیک در زمان های انتظار خود شروع به اجرای و نگهداری نتیجه دستوراتی می‌کند که هنوز زمان اجرای انها فرا نرسیده. با این کار وقتی لازم به اجرا آن دستورات باشد از قبل آنها اجرا شده و نتیجه مشخص است.

تکنیک Caching: یکی دیگر از تکنیک های افزایش سرعت پردازش است که نتیجه داده های محاسبه شده یا برخی داده های ضروری در یک حافظه بسیار پرسرعت در کنار پردازنده ذخیره می‌شوند.


سلسله مراتب cache
سلسله مراتب cache

وقتی بیش از یک هسته شروع به اجرای دستورات و تغییر داده‌ها می‌کند. اونها داده‌های موجود تو cache رو با هم تغییر می‌دن.

با توجه به memory hierarchy، در پردازش های موازی زمانی که داده ای بین core ها مشترک هستش ما نمی‌تونیم بگیم که همیشه همه کارها به همون ترتیبی که نوشتن شدن اجرا می‌شن. دلیل اصلی هم memory visibility و reordering هستش.

https://gist.github.com/Darvishiyan/cc153e5ada0107140ed56d9bdcd2f42a

قبل از شروع به مثال بالا نگاه کنید و فرض کنید که پردازنده thread ها رو توی core های متفاوتی اجرا می‌کنه.

  • توی main thread یک کپی از متغیر های number و ready وجود داره.
  • توی reader thread هم یک کپی از متغیر های number و ready وجود داره.

در پردازنده های جدید دستور نوشتن بلافاصله بعد از صادر شدن اجرا نمی‌شه. در واقع CPU دوست داره این دستورات رو بزاره تو صف توی special write buffer (بافر مخصوص نوشتن). و بعد از مدتی همه اونها رو یک مرتبه توی حافظه اصلی بنویسه.

این به این معنی هستش که وقتی در یک thread داده ای تغییر می‌کنه، هیچ تضمینی نیست که توی باقی thread ها هم اون داده تغییر کنه. به عبارت دیگه در مثال بالا ممکنه reader thread تغییرات داده ها رو بلافاصله یا با تاخیر دریافت کنه یا هرگز دریافت نکنه.

داده ها در یک thread به همون ترتیبی که توی یک thread دیگه داده تغییر می‌کنه تغییر رو دریافت نمی‌کنن. این یک روش بهینه سازی برای بهبود عملکرد هستش. قسمت های مختلف این بهینه سازی رو انجام می‌دن. و بهش می‌گیم reordering.

  • پردازشگر ممکنه داده های رو به ترتیبی غیر از ترتیبی که توی برنامه نوشته شده بنویسه.
  • پردازشگر ممکنه در اجرای برنامه از تکتیک out-of-order execution استفاده کنه.
  • کامپایلر JIT هم ممکنه با هدف بهینه سازی تغییری در ترتیب اجرای دستورات بده.

هدف volatile اطمینان از بروز شدن متغیرها توی باقی فضاهای حافظه استفاده هستش. با استفاده از این modifier برنامه در حال اجرا به پردازنده می‌گه که این متغیر و هر جایی که ازش استفاده شده رو reorder نکن. همچنین بلافاصله بعد از تغییر این متغیر همه cache ها رو بروز کن.

در برنامه نویسی multi thread دوتا قانون باید رعایت بشه.

  • یک: Mutual Exclusion یعنی فقط یک thread یک تغییر مهم رو ایجاد می‌کنه.
  • دو: Visibility یعنی بعد بوجود اومدن تغییر توی داده‌های مشترک، باید همه thread هایی که داده بین اونها مشترک هستش، تغییرات رو دریافت کنن.

متدها یا بلاک های synchronized هر دوی این شرایط رو برای ما فراهم می‌کنن. البته با کاهش پرفورمنس.

حالا زمانی که ما میخواییم چندین thread بتونن یک بلاک کد رو به صورت موازی اجرا کنن و لازمه مطمئن باشیم که همه از تغییرات متغیرهای مشترک آگاه هستن باید از volatile استفاده کنیم. تا مشکل visibility داده ها رو برای ما حل کنه.


استراتژی Happens-Before Ordering: این استراتژی در رابطه به متغیر های volatile می‌گه؛ نوشتن توی متغیر های volatile قبل از خوندن اونها اتفاق می‌یوفته. این یک قانون درباره متغیر های volatile در Java Memory Model هستش JMM.

تکنیک Piggybacking: با توجه به استراتژی Happens-Before در ترتیب حافظه. در بعضی شرایط ما می‌تونیم visibility یک متغیر رو piggyback (کول کردن - به دوش کشیدن) کنیم روی یک متغیر volatile دیگه.


https://gist.github.com/Darvishiyan/e405d378af536fd84057c65c90a0cb7b

تو مثال بالا ما هر دو متغیر رو volatile کردیم. که این کار باعث کاهش کارایی برنامه می‌شه.

استراتژی Happens-Before برای این مثال می‌گه: همه چیز باید بعد از تغییر متغیر ready و قبل از خوندن مقدار این متغیر visible باشه.

https://gist.github.com/Darvishiyan/e48cd1ba79684bceebe60022a823df5d

اما با توجه به این استراتژی ما می‌تونیم متغیر number رو به صورت معمولی تعریف کنیم و تغییرات اون piggyback کنیم روی ready یا به عبارت دیگه بر روی دوش متغیر ready اطلاعات این متغیر رو جابجا کنیم.

با توجه به این مفاهیم و با کمتر تعریف کردن متغیر های volatile ما می‌تونیم علاوه بر گارانتی کردن visibility کد بهینه تری هم بنویسیم.

http://tutorials.jenkov.com/java-concurrency/volatile.html
https://www.baeldung.com/java-volatile
https://dzone.com/articles/java-concurrency-understanding-the-volatile-keyword
https://www.geeksforgeeks.org/volatile-keyword-in-java/
javavolatilememorycpuprogramming
Android Engineer at Adevinta
شاید از این پست‌ها خوشتان بیاید