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âmetrofairdefine 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
Semaphorecontrola 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...finallypara garantir querelease()seja sempre chamado, prevenindo deadlocks.