آموزش قدم به قدم جاوا – قسمت بیست و ششم – بخش دوم

ارتباط thread ها با یکدیگر

گاهی اوقات در برنامه‌های چندنخی threadها باید به نوعی با هم ارتباط برقرار کنند. فرض کنید یک thread در حال اجرای کدی درون یک متد synchronized است و نیاز به دسترسی به منبعی (مثلا یک فایل) دارد که این منبع فعلا در دسترس نیست. از طرفی اگر thread منتظر باشد تا منبع در دسترس شود باعث می‌شود که بقیه threadها نتوانند به متد synchronized دسترسی پیدا کنند و این مسئله می‌تواند باعث منتظر ماندن چند thread و در نتیجه کندی برنامه شود.

راه بهتر برای سناریویی که گفته شد این است که thread ای که منتظر در دسترس شدن منبع است، بصورت موقت کنترل شی را رها کند تا شی unlock شود و دیگر thread ها بتوانند به متد synchronized دسترسی پیدا کنند و وقتی منبع در دسترس قرار گرفت به thread اولی اطلاع داده شود تا بتواند به کار خود ادامه دهد و به منبع دسترسی پیدا کند.

این روش نیازمند مکانیزمی برای برقراری ارتباط بین threadهای در حال اجراست. این ارتباطات در جاوا توسط سه متد wait() و notify() و notifyAll() صورت می‌گیرد. این سه متد در کلاس Object وجود دارند و بنابراین تمام اشیا در جاوا از این سه متد برخوردارند. نکته مهم این‌که این سه متد حتما باید در یک محیط synchronized (مثل متدها یا بلاک‌های synchronized) فراخوانی شوند.

برای این‌که thread ای موقتا اجرای خود را متوقف کند باید متد wait را فراخوانی کند. فراخوانی این متد باعث می‌شود تا thread اصطلاحا به خواب برود و شیئی که thread به آن دسترسی داشته unlock شود تا thread های دیگر بتوانند از آن شی استفاده کنند.

از طرفی دیگر وقتی thread ای داخل monitor ای شود که thread دیگری در آن به خواب رفته است می‌تواند بعد از اتمام کار خود با فراخوانی متد notify یا notifyAll آن thread را بیدار کند.

یک بار فراخوانی notify باعث بیدار کردن و ادامه کار یک thread می‌شود و یک بار فراخوانی notifyAll باعث بیدار شدن تمام thread ها در monitor ای که این متدها از آن فراخوانی شده می‌شود که ابتدا thread با اولویت بالاتر به شی دسترسی پیدا می‌کند.

اگرچه فراخوانی wait باعث می‌شود که thread تا زمانی که متدهای notify یا notfiyAll فراخوانی نشده در حالت خواب باشد اما احتمال کمی وجود دارد که thread خود به خود به دلیل spurious wakeup بیدار شود. اگرچه این موقعیت به ندرت پیش می‌آید اما روشی برای مدیریت آن وجود دارد که در مثالی که در ادامه آورده می‌شود با آن آشنا خواهید شد.

مثال: کلاسی به نام TickTock به صورت زیر داریم. این کلاس عملکرد تیک تاک ساعت را شبیه‌سازی می‌کند:

این کلاس دارای دو متد به نام‌های tick و tock است که به نوعی با هم ارتباط برقرار می‌کنند تا چرخه‌ی “تیک تاک” برقرار باشد. همچنین فیلدی به نام state در این کلاس وجود دارد که یکی از دو مقدار ticked یا tocked را نگهداری خواهد کرد که بیانگر وضعیت فعلی ساعت است.

کلاسی به نام MyThread به صورت زیر داریم:

سازنده کلاس MyThread دو پارامتر می‌گیرد. اولی نام thread است که ما یکی از مقادیر Tick یا Tock را به آن خواهیم داد. دومی شیئی از کلاس TickTock است.

در متد run این کلاس چک می‌شود که اگر نام thread برابر با Tick بود متد tick فراخوانی شود و در غیر این صورت (وقتی نام thread برابر با Tock باشد) متد tock فراخوانی خواهد شد.

هر کدام از متدهای tick و tock در حلقه 5 بار با پارامتر true فراخوانی می‌شوند. ساعتی که نوشتیم تا وقتی که پارامتر متدهای آن true باشد اجرا خواهد شد و به محض این‌که پارامتر false به هر کدام از دو متد tick یا tock داده شود ساعت متوقف خواهد شد.

در واقع اصلی‌ترین بخش برنامه ما متدهای tick و tock هستند:

۱- متد tick به صورت synchronized تعریف شده و همانطور که گفتیم متدهای wait و notify را فقط در متدها یا بلاک‌های synchronized می‌توانیم فراخوانی کنیم.

۲- شروع کار متد با چک کردن مقدار پارامتر running می‌باشد. اگر مقدار این پارامتر false باشد به این معنی است که ساعت متوقف شده است که در این صورت مقدار متغیر state به ticked تغییر خواهد کرد و متد notify فراخوانی خواهد شد. دلیل فراخوانی متد notify در این قسمت را در ادامه متوجه خواهید شد.

۳- اما اگر ساعت متوقف نشده باشد کلمه Tick روی صفحه چاپ خواهد شد، مقدار متغیر state به ticked تغییر خواهد کرد و متد notify فراخوانی خواهد شد. فراخوانی notify باعث می‌شود تا thread دیگری بتواند به این شی دسترسی پیدا کند.

۴- سپس در یک حلقه تا وقتی که state به tocked تغییر نکرده متد wait فراخوانی می‌شود. دلیل فراخوانی wait در حلقه برای جلوگیری از بروز spurious wakeup است.

۵- در نتیجه با فراخوانی tick عبارت Tick چاپ شده و thread ای که tick را فراخوانی کرده به خواب می‌رود.

۶- متد tock نیز مانند tick عمل می‌کند منتها با این تفاوت که این متد عبارت Tock را چاپ کرده و مقدار state را به tocked تغییر می‌دهد.

۷- نتیجه این کدها این است که thread ای که نام آن Tick است متد tick را فراخوانی می‌کند و به خواب می‌رود و سپس thread ای که نام آن Tock است متد tock را فراخوانی می‌کند و thread اول را بیدار می‌کند و خود به خواب می‌رود و این چرخه به تعدادی که ما مشخص کردیم ادامه پیدا می‌کند بدون آنکه این دو thread با هم تداخل پیدا کنند.

۸- دلیل اینکه بعد از متوقف کردن ساعت (وقتی که running برابر با false شود) متد notify را فراخوانی کردیم این است که اگراین متد را فراخوانی نکنیم وقتی ساعت متوقف شود یکی از متدها برای آخرین بار wait را فراخوانی کرده و بنابراین یک thread در انتظار unlock شدن شی باقی می‌ماند و دیگر thread ای نیست که notify را صدا کند تا و بنابراین برنامه هنگ خواهد کرد. برای همین باید این متد را فراخوانی کنیم تا آخرین فراخوانی متد tick یا tock به پایان برسد.

حالات اجرای یک thread

یک thread می‌تواند در یکی از 6 حالت زیر باشد:

NEW

در این حالت thread ایجاد شده اما هنوز شروع نشده است.

RUNNABLE

در این حالت thread در حال اجراست.

BLOCKED

در این حالت thread منتظر است تا قفل مانیتور یک شی باز شود تا به آن دسترسی پیدا کند.

WAITING

در این حالت thread منتظر است تا thread ای دیگر کاری را انجام دهد. مانند مثالی که دیدیم.

TIMED_WAITING

این حالت مانند حالت قبل است با این تفاوت که انتظار thread در این حالت تا مدت زمان محدودی است یعنی مثلا 5 ثانیه و اگر زمان انتظار بیشتر از 5 ثانیه شد thread از این حالت خارج خواهد شد.

TERMINATED

در این حالت کار thread تمام شده و اجرای آن متوقف شده است.

حالاتی که ذکر شد را می‌توان با فراخوانی متد getState از یک شی از کلاس Thread به دست آورد.

threadهای Daemon

در یک دسته‌بندی می‌توان threadها را در جاوا به دو دسته daemon و nondaemon تقسیم کرد. برای فهم بهتر تفاوت این دو به کد زیر توجه کنید:

حاصل اجرای این کد در Main Thread چاپ اعداد 0 تا 9 است. در این کد Main Thread بعد از شروع کردن t پایان می‌یابد اما برنامه تا زمانی که تمام thread ها به پایان نرسند متوقف نخواهد شد. thread ای که در این کد دیدیم nondaemon بود.

کد قبل را به صورت زیر تغییر می‌دهیم:

حال اگر برنامه را اجرا کنیم هیچ چیزی چاپ نخواهد شد! دلیل آن هم این است که وقتی Main Thread به پایان برسد تمام threadهای daemon نیز به پایان می‌رسند حتی اگر کار خود را تمام نکرده باشند و برنامه منتظر پایان کار thread های daemon نخواهد ماند.

threadهای daemon معمولا به عنوان سرویس‌دهنده به thread های دیگر استفاده می‌شوند که وجود آن‌ها به تنهایی کار خاصی انجام نمی‌دهد و به نوعی به thread‌های nondaemon وابسته هستند و با پایان thread‌های nondaemon دلیلی برای باقی‌ماندن threadهای daemon وجود نخواهد داشت.

سخن آخر

مبحث چندنخی در جاوا گسترده است و فرصت اشاره به تمام موارد آن در یک یا دو مقاله وجود ندارد. منابع زیر برای مطالعه بیشتر مفید هستند:

Race Condition

متغیرهای Volatile

اولویت thread ها

Deadlock

Starvation و Livelock

مصطفی نصیری

دانشجوی نرم افزار هستم و علاقه شدیدی به برنامه نویسی مخصوصا با زبان جاوا دارم! در حال حاضر تمرکزم روی اندرویده. دوست دارم چیزایی که یاد میگیرم رو با بقیه به اشتراک بگذارم :)

همچنین ممکن است دوست داشته باشید ...

۲ واکنش

  1. خاتمی گفت:

    سلام
    می خواستم بدونم این آموزشهای جاوا ادامه دارند یا همینجا تموم شده
    با تشکر از جنابعالی

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *