Sémaphores en Java

Sémaphores en Java

1. Vue d'ensemble

Dans ce rapide didacticiel, nous allons explorer les bases des sémaphores et des mutex en Java.

2. Semaphore

Nous allons commencer parjava.util.concurrent.Semaphore. Nous pouvons utiliser des sémaphores pour limiter le nombre de threads simultanés accédant à une ressource spécifique.

Dans l'exemple suivant, nous allons implémenter une file d'attente de connexion simple pour limiter le nombre d'utilisateurs dans le système:

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();
    }

}

Notez comment nous avons utilisé les méthodes suivantes:

  • tryAcquire() - renvoie vrai si un permis est disponible immédiatement et l'acquiert sinon renvoie faux, maisacquire() acquiert un permis et le bloque jusqu'à ce qu'il en soit disponible

  • release () - libérer un permis

  • availablePermits() – renvoie le nombre de permis actuels disponibles

Pour tester notre file d'attente de connexion, nous allons d'abord essayer d'atteindre la limite et vérifier si la prochaine tentative de connexion sera bloquée:

@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());
}

Ensuite, nous verrons si des emplacements sont disponibles après une déconnexion:

@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());
}

3. TimedSemaphore

Ensuite, nous discuterons d'Apache CommonsTimedSemaphore.TimedSemaphore autorise un certain nombre de permis en tant que simple sémaphore mais dans une période de temps donnée, après cette période, le temps de réinitialisation et tous les permis sont libérés.

Nous pouvons utiliserTimedSemaphore pour construire une file d'attente de délai simple comme suit:

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();
    }

}

Lorsque nous utilisons une file d'attente avec une seconde comme période et après avoir utilisé tous les créneaux dans une seconde, aucune ne devrait être disponible:

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());
}

Mais après avoir dormi pendant un certain temps,the semaphore should reset and release the permits:

@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());
}

4. Sémaphore vs. Mutex

Le mutex agit comme un sémaphore binaire, on peut l'utiliser pour implémenter une exclusion mutuelle.

Dans l'exemple suivant, nous allons utiliser un simple sémaphore binaire pour créer un compteur:

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();
    }
}

Quand beaucoup de threads essaient d'accéder au compteur en même temps,they’ll simply be blocked in a queue:

@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());
}

Lorsque nous attendons, tous les threads accéderont au compteur et il ne restera plus de threads dans la file d'attente:

@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());
}

5. Conclusion

Dans cet article, nous avons exploré les bases des sémaphores en Java.

Comme toujours, le code source complet est disponibleover on GitHub.