استفاده از شیئ Mutex در جاوا – راهنمای جامع
در این راهنما در مورد روشهای مختلف پیادهسازی یک شیء Mutex در جاوا بحث خواهیم کرد. اما نخست باید با مفهوم Mutex آشنا شویم.
Mutex چیست؟
در اپلیکیشنهای «چندنخی» (Multithreaded)، دو یا چند نخ باید به طور همزمان به منبع مشترکی دسترسی پیدا کنند که منجر به رفتار غیرمنتظرهای میشود. مثالهایی از چنین منابع مشترکی، ساختمانهای داده، دستگاههای ورودی-خروجی، فایلها و اتصالهای شبکه هستند. این سناریو به صورت معمول به نام «شرایط رقابت» (Race Condition) خوانده میشود. آن بخشی از برنامه که به منبع مشترک دسترسی مییابد، «بخش حیاتی» (Critical Section) نام دارد. بنابراین برای جلوگیری از بروز شرایط رقابت، باید دسترسی به بخش حیانی را «همگامسازی» (Synchronize) کنیم.
یک Mutex یا «انحصار متقابل» (Mutual Exclusion) سادهترین نوع «همگامساز» (Synchronizer) محسوب میشود. این شیوه تضمین میکند که در هر لحظه، تنها یک نخ میتواند بخش حیاتی یک برنامه رایانهای را اجرا کند. برای دسترسی به بخش حیاتی یک نخ باید Mutex را به دست آورد، سپس به بخش حیاتی دست مییابد و در نهایت Mutex را آزاد میکند. در این زمان همه نخهای دیگر مسدود میشوند تا این که Mutex مجدداً آزادسازی شود. به محض این که نخ از بخش حیاتی خارج شود، نخ دیگری میتواند وارد بخش حیاتی شود.
چرا باید از Mutex استفاده کنیم؟
ابتدا یک مثال از کلاس SequenceGeneraror را در نظر بگیرید که عنصر بعدی دنباله را با افزایش یک واحد currentValue میسازد.
اکنون یک کلاس تست میسازیم تا ببینیم این متد در زمانی که چند نخ تلاش کنند به صورت همزمان به آن دسترسی پیدا کنند، به چه شیوهای عمل میکند:
زمانی که این کیس تست را اجرا کنیم، میبینیم که در اغلب موارد به دلایلی مشابه زیر از کار میافتد:
java.lang.AssertionError: expected:<1000> but was:<989> at org.junit.Assert.fail(Assert.java:88) at org.junit.Assert.failNotEquals(Assert.java:834) at org.junit.Assert.assertEquals(Assert.java:645)
تصور شده است که uniqueSequences اندازهای برابر با تعداد دفعات اجرای متد getNextSequence در کیس تست دارد. با این حال، این وضعیت به دلیل وجود «شرایط رقابت» برقرار نیست. بدیهی است که این رفتار مطلوبی نیست. بنابراین برای حل این مشکل شرایط رقابت باید مطمئن شویم که هر زمان تنها یک نخ میتواند متد getNextSequence را اجرا کند. در چنین سناریوهایی از یک Mutex برای همگامسازی نخها کمک میگیریم. روشهای مختلفی برای پیادهسازی یک mutex در جاوا وجود دارند. از این رو در ادامه به بررسی روشهای مختلف پیادهسازی یک Mutex برای کلاس SequenceGenerator میپردازیم.
استفاده از کلیدواژه synchronized
ابتدا به بررسی کلیدواژه synchronized میپردازیم که سادهترین روش برای پیادهسازی یک Mutex در جاوا محسوب میشود. هر شیء در جاوا یک «قفل داخلی» (intrinsic lock) دارد. متد synchronized و بلوک synchronized از این قفل داخلی برای محدودسازی دسترسی تنها یک نخ در هر زمان به بخش حیاتی استفاده میکنند.
از این رو زمانی که یک نخ اقدام به اجرای متد synchronized میکند یا وارد یک بلوک synchronized میشود، به طور خودکار یک قفل را به دست میآورد. این قفل زمانی که متد یا بلوک تکمیل شود یا استثنایی در آنها رخ دهد، آزاد خواهد شد. در ادامه getNextSequence را طوری تغییر میدهیم که یک Mutex داشته باشد. به این منظور کلیدواژه synchronized را به آن اضافه میکنیم:
بلوک synchronized مشابه متد synchronized است، اما کنترل بیشتری روی بخش حیاتی و شیئی که برای قفل کردن استفاده میکنیم، ارائه میکند. بنابراین در ادامه با شیوه استفاده از بلوک synchronized برای همگامسازی روی یک شیء mutex سفارشی آشنا میشویم:
استفاده از ReentrantLock
کلاس ReentrantLock در جاوا 1.5 معرفی شده است و انعطافپذیری و کنترل زیادی نسبت به رویکرد استفاده از کلیدواژه synchronized ارائه میکند. در ادامه شیوه استفاده از ReentrantLock برای دستیابی به «انحصار متقابل» را میبینید:
استفاده از Semaphore
کلاس Semaphore نیز همانند Semaphore در جاوا نسخه 1.5 معرفی شده است. Mutex تنها به یک نخ اجازه میدهد به بخش حیاتی دسترسی داشته باشد، اما Semaphore امکان دسترسی تعداد ثابتی نخ به یک بخش حیاتی را فراهم میسازد. از این رو در Semaphore با تعیین تعداد نخهای مجاز به دسترسی روی عدد یک، میتوانیم یک Mutex را پیادهسازی کنیم. در ادامه شیوه ایجاد نسخه «نخ-ایمن» (Thread-safe) دیگری از SequenceGenerator را با استفاده از Semaphore میبینید:
استفاده از کلاس Monitor در Guava
تا به اینجا دیدیم که گزینههای پیادهسازی Mutex با استفاده از قابلیتهای ارائه شده جاوا، مختلف هستند. با این حال، کلاس Monitor مربوط به کتابخانه Guava گوگل یک جایگزین بهتر برای کلاس ReentrantLock محسوب میشود. بر اساس مستندات (+) کدی که از Monitor استفاده میکند، خواناتر است و زمینه بروز خطای آن نسبت به کدی که از ReentrantLock استفاده میکند کمتر است. ابتدا باید وابستگی Maven مربوط به Guava را به پروژه اضافه کنیم:
اکنون زیرکلاس دیگری از SequenceGenerator را با استفاده از کلاس SequenceGenerator مینویسیم:
سخن پایانی
در این راهنما به بررسی مفهوم Mutex در جاوا پرداختیم. همچنین روشهای مختلف پیادهسازی آن را در جاوا مورد بررسی قرار دادیم. کد کامل موارد مطرحشده در این مقاله را میتوانید در این ریپوی گیتهاب (+) مشاهده کنید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای جاوا (Java)
- مجموعه آموزشهای برنامهنویسی
- گنجینه آموزش های جاوا (Java)
- زبان برنامه نویسی جاوا (Java) — از صفر تا صد
- آشنایی با امکانات جدید جاوا ۱۴ — راهنمای کاربردی
==