استفاده از شیئ Mutex در جاوا — راهنمای جامع

۳۴۳ بازدید
آخرین به‌روزرسانی: ۵ شهریور ۱۴۰۲
زمان مطالعه: ۴ دقیقه
استفاده از شیئ Mutex در جاوا — راهنمای جامع

در این راهنما در مورد روش‌های مختلف پیاده‌سازی یک شیء Mutex در جاوا بحث خواهیم کرد. اما نخست باید با مفهوم Mutex آشنا شویم.

997696

 Mutex چیست؟

در اپلیکیشن‌های «چندنخی» (Multithreaded)، دو یا چند نخ باید به طور هم‌زمان به منبع مشترکی دسترسی پیدا کنند که منجر به رفتار غیرمنتظره‌ای می‌شود. مثال‌هایی از چنین منابع مشترکی، ‌ساختمان‌های داده، دستگاه‌های ورودی-خروجی، فایل‌ها و اتصال‌های شبکه هستند. این سناریو به صورت معمول به نام «شرایط رقابت» (Race Condition)‌ خوانده می‌شود. آن بخشی از برنامه که به منبع مشترک دسترسی می‌یابد، «بخش حیاتی» (‌Critical Section) نام دارد. بنابراین برای جلوگیری از بروز شرایط رقابت، باید دسترسی به بخش حیانی را «همگام‌سازی» (Synchronize) کنیم.

یک Mutex یا «انحصار متقابل» (Mutual Exclusion) ساده‌ترین نوع «همگام‌ساز» (Synchronizer) محسوب می‌شود. این شیوه تضمین می‌کند که در هر لحظه، تنها یک نخ می‌تواند بخش حیاتی یک برنامه رایانه‌ای را اجرا کند. برای دسترسی به بخش حیاتی یک نخ باید Mutex را به دست آورد، سپس به بخش حیاتی دست می‌یابد و در نهایت Mutex را آزاد می‌کند. در این زمان همه نخ‌های دیگر مسدود می‌شوند تا این که Mutex مجدداً آزادسازی شود. به محض این که نخ از بخش حیاتی خارج شود، نخ دیگری می‌تواند وارد بخش حیاتی شود.

چرا باید از Mutex استفاده کنیم؟

ابتدا یک مثال از کلاس SequenceGeneraror را در نظر بگیرید که عنصر بعدی دنباله را با افزایش یک واحد currentValue می‌سازد.

1public class SequenceGenerator {
2     
3    private int currentValue = 0;
4 
5    public int getNextSequence() {
6        currentValue = currentValue + 1;
7        return currentValue;
8    }
9 
10}

اکنون یک کلاس تست می‌سازیم تا ببینیم این متد در زمانی که چند نخ تلاش کنند به صورت هم‌زمان به آن دسترسی پیدا کنند، به چه شیوه‌ای عمل می‌کند:

1@Test
2public void givenUnsafeSequenceGenerator_whenRaceCondition_thenUnexpectedBehavior() throws Exception {
3    int count = 1000;
4    Set<Integer> uniqueSequences = getUniqueSequences(new SequenceGenerator(), count);
5    Assert.assertEquals(count, uniqueSequences.size());
6}
7 
8private Set<Integer> getUniqueSequences(SequenceGenerator generator, int count) throws Exception {
9    ExecutorService executor = Executors.newFixedThreadPool(3);
10    Set<Integer> uniqueSequences = new LinkedHashSet<>();
11    List<Future<Integer>> futures = new ArrayList<>();
12 
13    for (int i = 0; i < count; i++) {
14        futures.add(executor.submit(generator::getNextSequence));
15    }
16 
17    for (Future<Integer> future : futures) {
18        uniqueSequences.add(future.get());
19    }
20 
21    executor.awaitTermination(1, TimeUnit.SECONDS);
22    executor.shutdown();
23 
24    return uniqueSequences;
25}

زمانی که این کیس تست را اجرا کنیم، می‌بینیم که در اغلب موارد به دلایلی مشابه زیر از کار می‌افتد:

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 را به آن اضافه می‌کنیم:

1public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator {
2     
3    @Override
4    public synchronized int getNextSequence() {
5        return super.getNextSequence();
6    }
7 
8}

بلوک synchronized مشابه متد synchronized است، اما کنترل بیشتری روی بخش حیاتی و شیئی که برای قفل کردن استفاده می‌کنیم، ارائه می‌کند. بنابراین در ادامه با شیوه استفاده از بلوک synchronized برای همگام‌سازی روی یک شیء mutex سفارشی آشنا می‌شویم:

1public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator {
2     
3    private Object mutex = new Object();
4 
5    @Override
6    public int getNextSequence() {
7        synchronized (mutex) {
8            return super.getNextSequence();
9        }
10    }
11 
12}

استفاده از ReentrantLock

کلاس ReentrantLock در جاوا 1.5 معرفی شده است و انعطاف‌پذیری و کنترل زیادی نسبت به رویکرد استفاده از کلیدواژه synchronized ارائه می‌کند. در ادامه شیوه استفاده از ReentrantLock برای دستیابی به «انحصار متقابل» را می‌بینید:

1public class SequenceGeneratorUsingReentrantLock extends SequenceGenerator {
2     
3    private ReentrantLock mutex = new ReentrantLock();
4 
5    @Override
6    public int getNextSequence() {
7        try {
8            mutex.lock();
9            return super.getNextSequence();
10        } finally {
11            mutex.unlock();
12        }
13    }
14}

استفاده از Semaphore

کلاس Semaphore نیز همانند Semaphore در جاوا نسخه 1.5 معرفی شده است. Mutex تنها به یک نخ اجازه می‌دهد به بخش حیاتی دسترسی داشته باشد، اما Semaphore امکان دسترسی تعداد ثابتی نخ به یک بخش حیاتی را فراهم می‌سازد. از این رو در Semaphore با تعیین تعداد نخ‌های مجاز به دسترسی روی عدد یک، می‌توانیم یک Mutex را پیاده‌سازی کنیم. در ادامه شیوه ایجاد نسخه «نخ-ایمن» (Thread-safe) دیگری از SequenceGenerator را با استفاده از Semaphore می‌بینید:

1public class SequenceGeneratorUsingSemaphore extends SequenceGenerator {
2     
3    private Semaphore mutex = new Semaphore(1);
4 
5    @Override
6    public int getNextSequence() {
7        try {
8            mutex.acquire();
9            return super.getNextSequence();
10        } catch (InterruptedException e) {
11            // exception handling code
12        } finally {
13            mutex.release();
14        }
15    }
16}

استفاده از کلاس Monitor در Guava

تا به اینجا دیدیم که گزینه‌های پیاده‌سازی Mutex با استفاده از قابلیت‌های ارائه شده جاوا، مختلف هستند. با این حال، کلاس Monitor مربوط به کتابخانه Guava گوگل یک جایگزین بهتر برای کلاس ReentrantLock محسوب‌ می‌شود. بر اساس مستندات (+) ‌کدی که از Monitor استفاده می‌کند، خواناتر است و زمینه بروز خطای آن نسبت به کدی که از ReentrantLock استفاده می‌کند کمتر است. ابتدا باید وابستگی Maven مربوط به Guava را به پروژه اضافه کنیم:

1<dependency>
2    <groupId>com.google.guava</groupId>
3    <artifactId>guava</artifactId>
4    <version>28.0-jre</version>
5</dependency>

اکنون زیرکلاس دیگری از SequenceGenerator را با استفاده از کلاس SequenceGenerator می‌نویسیم:

1public class SequenceGeneratorUsingMonitor extends SequenceGenerator {
2     
3    private Monitor mutex = new Monitor();
4 
5    @Override
6    public int getNextSequence() {
7        mutex.enter();
8        try {
9            return super.getNextSequence();
10        } finally {
11            mutex.leave();
12        }
13    }
14 
15}

سخن پایانی

در این راهنما به بررسی مفهوم Mutex در جاوا پرداختیم. همچنین روش‌های مختلف پیاده‌سازی آن را در جاوا مورد بررسی قرار دادیم. کد کامل موارد مطرح‌شده در این مقاله را می‌توانید در این ریپوی گیت‌هاب (+) مشاهده کنید.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۵ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
baeldung
نظر شما چیست؟

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