Semáforos em Java

Semáforos em Java

1. Visão geral

Neste tutorial rápido, vamos explorar os conceitos básicos de semáforos e mutexes em Java.

2. Semaphore

Vamos começar comjava.util.concurrent.Semaphore. Podemos usar semáforos para limitar o número de threads simultâneos acessando um recurso específico.

No exemplo a seguir, implementaremos uma fila de login simples para limitar o número de usuários no 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();
    }

}

Observe como usamos os seguintes métodos:

  • tryAcquire() - retorna verdadeiro se uma licença está disponível imediatamente e adquire-a, caso contrário retorna falso, masacquire() adquire uma licença e bloqueia até que uma esteja disponível

  • release () - libera uma licença

  • availablePermits() – número de retorno de licenças atuais disponíveis

Para testar nossa fila de logon, primeiro tentaremos atingir o limite e verificar se a próxima tentativa de logon será bloqueada:

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

A seguir, veremos se há slots disponíveis após um 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. CronometradoSemaphore

A seguir, discutiremos o Apache CommonsTimedSemaphore.TimedSemaphore permite uma série de licenças como um simples Semáforo, mas em um determinado período de tempo, após esse período o tempo zerado e todas as licenças são liberadas.

Podemos usarTimedSemaphore para construir uma fila de atraso simples da seguinte maneira:

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 usamos uma fila de atraso com um segundo como período de tempo e depois de usar todos os slots em um segundo, nenhum deve estar disponível:

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

Mas depois de dormir por algum tempo,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. Semáforo vs. Mutex

O Mutex age de maneira semelhante a um semáforo binário, podemos usá-lo para implementar exclusão mútua.

No exemplo a seguir, usaremos um semáforo binário simples para construir um contador:

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 muitos threads tentam acessar o contador de uma vez,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());
}

Quando esperamos, todos os threads acessam o contador e nenhum thread permanece na fila:

@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. Conclusão

Neste artigo, exploramos o básico dos semáforos em Java.

Como sempre, o código-fonte completo está disponívelover on GitHub.