Semafori in Java

1. Panoramica

In questo breve tutorial, esploreremo le basi di semafori e mutex in Java.

2. Semaforo

Inizieremo con java.util.concurrent.Semaphore. Possiamo usare i semafori per limitare il numero di thread simultanei che accedono a una risorsa specifica.

Nell'esempio seguente, implementeremo una semplice coda di accesso per limitare il numero di utenti nel sistema:

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

Nota come abbiamo utilizzato i seguenti metodi:

  • tryAcquire () - restituisce true se un permesso è disponibile immediatamente e lo acquisisce altrimenti restituisce false, ma acquisisce () acquisisce un permesso e lo blocca finché non è disponibile
  • release () - rilascia un permesso
  • availablePermits () - restituisce il numero di permessi correnti disponibili

Per testare la nostra coda di accesso, proveremo prima a raggiungere il limite e verificheremo se il prossimo tentativo di accesso verrà bloccato:

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

Successivamente, vedremo se qualche slot è disponibile dopo un logout:

@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. Semaforo a tempo

Successivamente, discuteremo di Apache Commons TimedSemaphore. TimedSemaphore consente un numero di permessi come un semplice semaforo ma in un dato periodo di tempo, dopo questo periodo il tempo si azzera e tutti i permessi vengono rilasciati.

Possiamo usare TimedSemaphore per costruire una semplice coda di ritardo come segue:

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

Quando usiamo una coda di ritardo con un secondo come periodo di tempo e dopo aver utilizzato tutti gli slot entro un secondo, nessuno dovrebbe essere disponibile:

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

Ma dopo aver dormito per un po 'di tempo, il semaforo dovrebbe resettare e rilasciare i permessi :

@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. Semaforo contro Mutex

Mutex agisce in modo simile a un semaforo binario, possiamo usarlo per implementare l'esclusione reciproca.

Nell'esempio seguente, utilizzeremo un semplice semaforo binario per creare un contatore:

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

Quando molti thread tentano di accedere al contatore contemporaneamente, verranno semplicemente bloccati in una coda :

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

Quando aspettiamo, tutti i thread accedono al contatore e nessun thread rimane nella coda:

@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. conclusione

In questo articolo, abbiamo esplorato le basi dei semafori in Java.

Come sempre, il codice sorgente completo è disponibile su GitHub.