در این راهنمای کوتاه از سری مقالات آموزش جامع جاوا، به بررسی مبانی Semaphore در جاوا و همچنین mutex-ها می‌پردازیم.

Semaphore چیست؟

توضیح خود را از java.util.concurrent.Semaphore آغاز می‌کنیم. از Semaphore می‌توان برای محدودسازی تعداد نخ‌هایی که به صورت هم‌زمان به یک منبع خاص دسترسی می‌یابند استفاده کرد. در مثال زیر، یک صف لاگین ساده برای محدود کردن تعداد کاربران در سیستم پیاده‌سازی می‌کنیم:

	
class LoginQueueUsingSemaphore {
 
    private Semaphore semaphore;
 
    public LoginQueueUsingSemaphore(int slotLimit) {
        semaphore = new Semaphore(slotLimit);
    }
 
    boolean tryLogin() {
        return semaphore.tryAcquire();
    }
 
    void logout() {
        semaphore.release();
    }
 
    int availableSlots() {
        return semaphore.availablePermits();
    }
 
}

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

  • ()tryAcquire – این متد در صورتی که یک مجوز، بی‌درنگ موجود باشد، مقدار true بازگشت داده و آن را در اختیار می‌گیرد، در غیر این صورت مقدار false بازگشت می‌دهد، اما ()acquire نیازمند یک مجوز است و تا زمانی که چنین مجوزی موجود شود، مسدود می‌شود.
  • ()release – یک مجوز را آزاد می‌کند.
  • ()availablePermits – این متد تعداد مجوزهایی که هم اینک موجود است را بازگشت می‌دهد.

برای تست صف لاگین، ابتدا باید به محدودیت برسیم و بررسی کنیم آیا تلاش لاگین بعدی مسدود خواهد شد یا نه:

@Test
public void givenLoginQueue_whenReachLimit_thenBlocked() {
    int slots = 10;
    ExecutorService executorService = Executors.newFixedThreadPool(slots);
    LoginQueueUsingSemaphore loginQueue = new LoginQueueUsingSemaphore(slots);
    IntStream.range(0, slots)
      .forEach(user -> executorService.execute(loginQueue::tryLogin));
    executorService.shutdown();
 
    assertEquals(0, loginQueue.availableSlots());
    assertFalse(loginQueue.tryLogin());
}

سپس بررسی می‌کنیم آیا هیچ اسلاتی پس از لاگ‌آوت موجود است یا نه:

@Test
public void givenLoginQueue_whenLogout_thenSlotsAvailable() {
    int slots = 10;
    ExecutorService executorService = Executors.newFixedThreadPool(slots);
    LoginQueueUsingSemaphore loginQueue = new LoginQueueUsingSemaphore(slots);
    IntStream.range(0, slots)
      .forEach(user -> executorService.execute(loginQueue::tryLogin));
    executorService.shutdown();
    assertEquals(0, loginQueue.availableSlots());
    loginQueue.logout();
 
    assertTrue(loginQueue.availableSlots() > 0);
    assertTrue(loginQueue.tryLogin());
}

Semaphore زمان‌دار

در این بخش به بررسی Apache Commons TimedSemaphore می‌پردازیم. TimedSemaphore به ما امکان می‌دهد که مانند Semaphore ساده چندین مجوز داشته باشیم، اما این مجوزها صرفاً در یک دوره زمانی مفروض موجود هستند و پس از طی شدن این دوره، زمان ریست می‌شود و همه مجوزها آزاد می‌شوند.

از TimedSemaphore می‌توان برای ساختن یک صف با تأخیر مانند زیر استفاده کرد:

class DelayQueueUsingTimedSemaphore {
 
    private TimedSemaphore semaphore;
 
    DelayQueueUsingTimedSemaphore(long period, int slotLimit) {
        semaphore = new TimedSemaphore(period, TimeUnit.SECONDS, slotLimit);
    }
 
    boolean tryAdd() {
        return semaphore.tryAcquire();
    }
 
    int availableSlots() {
        return semaphore.getAvailablePermits();
    }
 
}

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

public void givenDelayQueue_whenReachLimit_thenBlocked() {
    int slots = 50;
    ExecutorService executorService = Executors.newFixedThreadPool(slots);
    DelayQueueUsingTimedSemaphore delayQueue 
      = new DelayQueueUsingTimedSemaphore(1, slots);
     
    IntStream.range(0, slots)
      .forEach(user -> executorService.execute(delayQueue::tryAdd));
    executorService.shutdown();
 
    assertEquals(0, delayQueue.availableSlots());
    assertFalse(delayQueue.tryAdd());
}

اما پس از خوابیدن به مدت یک ثانیه، semaphore ریست شده و مجوزها را آزاد می‌کند:

@Test
public void givenDelayQueue_whenTimePass_thenSlotsAvailable() throws InterruptedException {
    int slots = 50;
    ExecutorService executorService = Executors.newFixedThreadPool(slots);
    DelayQueueUsingTimedSemaphore delayQueue = new DelayQueueUsingTimedSemaphore(1, slots);
    IntStream.range(0, slots)
      .forEach(user -> executorService.execute(delayQueue::tryAdd));
    executorService.shutdown();
 
    assertEquals(0, delayQueue.availableSlots());
    Thread.sleep(1000);
    assertTrue(delayQueue.availableSlots() > 0);
    assertTrue(delayQueue.tryAdd());
}

Semaphore در برابر Mutex

Mutex مشابه یک semaphore دودویی عمل می‌کند، ما می‌توانیم از آن برای پیاده‌سازی «انحصار متقابل» (Mutual Exclusion) استفاده کنیم. در مثال زیر، از یک semaphore ساده دودویی برای ساخت یک شمارنده استفاده کرده‌ایم:

class CounterUsingMutex {
 
    private Semaphore mutex;
    private int count;
 
    CounterUsingMutex() {
        mutex = new Semaphore(1);
        count = 0;
    }
 
    void increase() throws InterruptedException {
        mutex.acquire();
        this.count = this.count + 1;
        Thread.sleep(1000);
        mutex.release();
 
    }
 
    int getCount() {
        return this.count;
    }
 
    boolean hasQueuedThreads() {
        return mutex.hasQueuedThreads();
    }
}

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

@Test
public void whenMutexAndMultipleThreads_thenBlocked()
 throws InterruptedException {
    int count = 5;
    ExecutorService executorService
     = Executors.newFixedThreadPool(count);
    CounterUsingMutex counter = new CounterUsingMutex();
    IntStream.range(0, count)
      .forEach(user -> executorService.execute(() -> {
          try {
              counter.increase();
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }));
    executorService.shutdown();
 
    assertTrue(counter.hasQueuedThreads());
}

زمانی که صبر کنیم، همه نخ‌ها به شمارنده دسترسی می‌یابند و هیچ نخی در صف باقی نمی‌ماند:

@Test
public void givenMutexAndMultipleThreads_ThenDelay_thenCorrectCount()
 throws InterruptedException {
    int count = 5;
    ExecutorService executorService
     = Executors.newFixedThreadPool(count);
    CounterUsingMutex counter = new CounterUsingMutex();
    IntStream.range(0, count)
      .forEach(user -> executorService.execute(() -> {
          try {
              counter.increase();
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }));
    executorService.shutdown();
 
    assertTrue(counter.hasQueuedThreads());
    Thread.sleep(5000);
    assertFalse(counter.hasQueuedThreads());
    assertEquals(count, counter.getCount());
}

سخن پایانی

در این مقاله به بررسی مبانی مقدماتی semaphore در جاوا پرداختیم. کد همه موارد مطرح‌شده در این مقاله را می‌توانید در این ریپوی گیت‌هاب (+) ‌ببینید:

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

==

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
شما قبلا رای داده‌اید!
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.

«میثم لطفی» در رشته‌های ریاضیات کاربردی و مهندسی کامپیوتر به تحصیل پرداخته و شیفته فناوری است. وی در حال حاضر علاوه بر پیگیری علاقه‌مندی‌هایش در رشته‌های برنامه‌نویسی، کپی‌رایتینگ و محتوای چندرسانه‌ای، در زمینه نگارش مقالاتی با محوریت نرم‌افزار با مجله فرادرس همکاری دارد.

نظر شما چیست؟

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