Controle de Acesso com Semáforos em Java
Semáforos são uma ferramenta de sincronização que servem para controlar o acesso concorrente a um recurso. Em Java, eles são representados pela classe Semaphore
, localizada no pacote java.util.concurrent
.
Um semáforo funciona como um controlador de acesso baseado em um contador. Esse contador define quantas threads podem acessar um recurso ao mesmo tempo. A lógica é a seguinte:
Para entrar: Quando uma thread quer usar o recurso, ela chama o método
acquire()
.- Se o contador for maior que zero, a thread tem a passagem liberada e o contador diminui em um.
- Se o contador for zero, a thread fica bloqueada e precisa esperar.
Para sair: Quando a thread termina de usar o recurso, ela chama o método
release()
.- Isso faz o contador aumentar em um, permitindo que uma das threads que estava esperando possa finalmente acessar o recurso.
O número de permissões é definido no construtor da classe Semaphore
:
Semaphore(int permits)
: Cria um semáforo com o número especificado de permissões.Semaphore(int permits, boolean fair)
: O parâmetrofair
define a política de aquisição. Setrue
(justo), as permissões são concedidas às threads na ordem em que elas as solicitaram (FIFO). Sefalse
(injusto, o padrão), a ordem não é garantida, o que pode levar a uma maior performance, mas também ao risco de starvation (uma thread nunca conseguir a permissão).
Para obter uma permissão, utiliza-se o método acquire()
. Após o trabalho, a permissão deve ser liberada com release()
.
Exemplo Básico de Semáforo
Vamos usar um semáforo com uma permissão para garantir que apenas uma thread por vez possa modificar um recurso compartilhado.
import java.util.concurrent.Semaphore;
class CommonResource {
int x = 0;
}
class CountThread implements Runnable {
CommonResource res;
Semaphore sem;
String name;
CountThread(CommonResource res, Semaphore sem, String name) {
this.res = res;
this.sem = sem;
this.name = name;
}
public void run() {
try {
System.out.println(name + " aguardando permissão...");
sem.acquire(); // Solicita uma permissão, bloqueia se não houver
// --- Início da Seção Crítica ---
res.x = 1;
for (int i = 1; i < 5; i++) {
System.out.println(this.name + ": " + res.x);
res.x++;
Thread.sleep(100);
}
// --- Fim da Seção Crítica ---
} catch (InterruptedException e) {
System.out.println(e.getMessage());
} finally {
System.out.println(name + " liberando permissão.");
sem.release(); // Libera a permissão
}
}
}
public class Program {
public static void main(String[] args) {
Semaphore sem = new Semaphore(1); // Apenas 1 permissão
CommonResource res = new CommonResource();
new Thread(new CountThread(res, sem, "CountThread 1")).start();
new Thread(new CountThread(res, sem, "CountThread 2")).start();
new Thread(new CountThread(res, sem, "CountThread 3")).start();
}
}
É uma prática de segurança essencial colocar a chamada sem.release()
dentro de um bloco finally
. Isso garante que a permissão seja sempre liberada, mesmo que uma exceção ocorra dentro da seção crítica, prevenindo que o semáforo fique permanentemente bloqueado (deadlock).
A saída do programa mostrará as threads executando a seção crítica em uma ordem não determinística, uma de cada vez:
CountThread 1 aguardando permissão... CountThread 2 aguardando permissão... CountThread 3 aguardando permissão... CountThread 1: 1 CountThread 1: 2 CountThread 1: 3 CountThread 1: 4 CountThread 1 liberando permissão. CountThread 3: 1 CountThread 3: 2 CountThread 3: 3 CountThread 3: 4 CountThread 3 liberando permissão. CountThread 2: 1 ...
O Problema dos Filósofos na Janta
Semáforos são ideais para limitar o acesso a um número finito de recursos. Um exemplo clássico é o "Problema dos Filósofos na Janta": imagine uma mesa com cinco filósofos, mas apenas duas cadeiras. No máximo dois podem se sentar à mesa simultaneamente.
import java.util.concurrent.Semaphore;
class Philosopher extends Thread {
private Semaphore sem;
private int mealsEaten = 0;
private int id;
Philosopher(Semaphore sem, int id) {
this.sem = sem;
this.id = id;
}
public void run() {
try {
while (mealsEaten < 3) {
sem.acquire(); // Pega um lugar à mesa
try {
System.out.println("Filósofo " + id + " senta-se à mesa.");
sleep(500); // Filósofo está comendo
mealsEaten++;
System.out.println("Filósofo " + id + " levanta-se da mesa.");
} finally {
sem.release(); // Libera o lugar à mesa
}
sleep(500); // Filósofo está pensando/passeando
}
} catch (InterruptedException e) {
System.out.println("Filósofo " + id + " teve problemas e se retirou.");
}
}
}
public class Program {
public static void main(String[] args) {
Semaphore table = new Semaphore(2); // Apenas 2 lugares na mesa
for (int i = 1; i <= 5; i++) {
new Philosopher(table, i).start();
}
}
}
A saída do programa mostrará que nunca haverá mais de dois filósofos "sentados à mesa" ao mesmo tempo, demonstrando o controle de capacidade do semáforo:
Filósofo 1 senta-se à mesa. Filósofo 2 senta-se à mesa. Filósofo 1 levanta-se da mesa. Filósofo 3 senta-se à mesa. Filósofo 2 levanta-se da mesa. Filósofo 4 senta-se à mesa. Filósofo 3 levanta-se da mesa. Filósofo 5 senta-se à mesa. Filósofo 4 levanta-se da mesa. Filósofo 5 levanta-se da mesa. Filósofo 1 senta-se à mesa. ...
Resumo
- Um
Semaphore
controla o acesso a recursos através de um contador de permissões, permitindo acesso concorrente limitado. - O método
acquire()
obtém uma permissão, bloqueando a thread se nenhuma estiver disponível. - O método
release()
devolve uma permissão ao semáforo, potencialmente liberando uma thread em espera. - É crucial usar um bloco
try...finally
para garantir querelease()
seja sempre chamado, prevenindo deadlocks.