Sémaphores en Java

1. Vue d'ensemble

Dans ce rapide tutoriel, nous explorerons les bases des sémaphores et des mutex en Java.

2. Sémaphore

Nous allons commencer par java.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(); } }

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

  • tryAcquire () - retourne true si un permis est disponible immédiatement et l'acquiert sinon renvoie false, mais acquiert () acquiert un permis et bloque jusqu'à ce qu'il en soit un 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. Sémaphore chronométré

Ensuite, nous discuterons d'Apache Commons TimedSemaphore. TimedSemaphore permet un certain nombre de permis comme un simple sémaphore, mais dans une période de temps donnée, après cette période, la réinitialisation de l'heure et tous les permis sont libérés.

Nous pouvons utiliser TimedSemaphore pour créer 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 de temps et après avoir utilisé tous les créneaux horaires en une seconde, aucun 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, le sémaphore devrait se réinitialiser et libérer les autorisations :

@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 contre Mutex

Mutex agit de la même manière qu'un sémaphore binaire, nous pouvons l'utiliser pour implémenter l'exclusion mutuelle.

Dans l'exemple suivant, nous utiliserons 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, ils seront simplement bloqués dans une file d'attente :

@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 aucun thread ne restera 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 disponible à l'adresse over sur GitHub.