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.