Controle de Threads Seguras em Java
Na programação multithread, garantir a segurança do acesso a recursos compartilhados é fundamental. Enquanto abordagens básicas utilizam classes thread-safe como o Vector, esta exploração foca no uso de mecanismos de bloqueio (locks) para controlar o acesso concorrente.
Uso de Sincronização
A palavra-chave synchronized em Java é essencial para controlar o acesso a métodos ou blocos de código por múltiplas threads. Quando um método é marcado como synchronized, apenas uma thread pode executá por vez em um determinado objeto.
Existem duas formas principais de utilizar a sincronização:
- Métodos sincronizados: ```
public synchronized void metodoCritico() {
// Código que requer proteção contra acesso concorrente
}
- Blocos sincronizados: ```
synchronized (objetoReferencia) {
// Código que requer proteção contra acesso concorrente
}
A falta de sincronização pode levar a condições de corrida, onde múltiplas threads modificam um recurso compartilhado simultaneamente, resultando em inconsistências.
Mecanismos de Controle de Threads
Wait()
O método Object.wait() coloca a thread atual em estado de espera até que outra thread a notifique. A thread permanece no estado de bloqueado até receber uma notificação ou até que o tempo de espera expire.
É importante notar que wait() opera no objeto monitor, não diretamente na thread. Quando múltiplas threads operam no mesmo objeto, uma entrando em espera não afeta outras threads que continuam operando no mesmo objeto.
Sleep()
Thread.sleep(tempoMilissegundos) suspende a execução da thread atual por um período especificado. Diferente de wait(), sleep() não libera os bloqueios que a thread pode possuir.
Notify() e NotifyAll()
Object.notify(): Acorda uma única thread que está esperando no monitor deste objeto. A thread específica a ser acordada é escolhida pela JVM.Object.notifyAll(): Acorda todas as threads que estão esperando no monitor deste objeto.
Modelo Produtor-Consumidor
O padrão produtor-consumidor é um clássico exemplo de problema de concorrência onde:
- Múltiplos produtores adicionam itens a um recurso compartilhado
- Múltiplos consumidores removem itens desse mesmo recurso
- O acesso ao recurso compartilhado deve ser sincronizado
Implementação em Java
Classe Principal
package concorrencia;
public class SistemaConcorrente {
public static void main(String[] args) {
Deposito deposito = Deposito.getInstancia();
Consumidor c1 = new Consumidor("Consumidor 1", deposito);
Consumidor c2 = new Consumidor("Consumidor 2", deposito);
Consumidor c3 = new Consumidor("Consumidor 3", deposito);
Produtor p1 = new Produtor("Produtor 1", deposito);
Produtor p2 = new Produtor("Produtor 2", deposito);
// Configurando prioridades
p1.setPriority(8);
p2.setPriority(8);
// Iniciando as threads
c1.start();
c2.start();
c3.start();
p1.start();
p2.start();
}
}
Classe Consumidor
package concorrencia;
public class Consumidor extends Thread {
private final String nome;
private final Deposito deposito;
public Consumidor(String nome, Deposito deposito) {
this.nome = nome;
this.deposito = deposito;
}
@Override
public void run() {
for (int i = 0; i < 15; i++) {
adquirirItem();
try {
Thread.sleep((long) (Math.random() * 100));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private void adquirirItem() {
Item item = deposito.removerItem();
if (item != null) {
System.out.println(nome + " adquiriu " + item.getNome());
} else {
System.out.println(nome + " tentou adquirir item, mas o estoque está vazio!");
}
}
}
Classe Produtor
package concorrencia;
public class Produtor extends Thread {
private final String nome;
private final Deposito deposito;
private int contadorItens = 1;
public Produtor(String nome, Deposito deposito) {
this.nome = nome;
this.deposito = deposito;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
produzirItem();
try {
Thread.sleep((long) (Math.random() * 150));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private void produzirItem() {
Item item = new Item("Item-" + (contadorItens++));
deposito.adicionarItem(item);
System.out.println(nome + " produziu " + item.getNome());
}
}
Classe Depósito (Recurso Compartilhado)
package concorrencia;
import java.util.ArrayList;
import java.util.List;
public class Deposito {
private static final int CAPACIDADE_MAXIMA = 30;
private static Deposito instancia;
private final List<item> itens;
private int contadorItens = 1;
private Deposito() {
this.itens = new ArrayList<>();
// Inicialização com alguns itens
for (int i = 0; i < 10; i++) {
itens.add(new Item("Item-" + (contadorItens++)));
}
}
public static synchronized Deposito getInstancia() {
if (instancia == null) {
instancia = new Deposito();
}
return instancia;
}
public synchronized Item removerItem() {
if (itens.isEmpty()) {
try {
System.out.println("Todos os consumidores aguardando: estoque vazio!");
notifyAll(); // Notifica os produtores que precisam repor
wait(); // Aguarda novos itens
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
if (!itens.isEmpty()) {
return itens.remove(0);
}
return null;
}
public synchronized void adicionarItem(Item item) {
if (itens.size() >= CAPACIDADE_MAXIMA) {
try {
wait(); // Aguarda espaço no estoque
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
itens.add(item);
if (itens.size() == 1) {
notifyAll(); // Notifica consumidores que há itens disponíveis
}
}
public int getQuantidadeAtual() {
return itens.size();
}
}</item>
Classe Item
package concorrencia;
public class Item {
private final String nome;
public Item(String nome) {
this.nome = nome;
}
public String getNome() {
return nome;
}
}
Nesta implementação, o padrão produtor-consumidor utiliza sincronização para garantir que o acesso ao depósito seja thread-safe. Os produtores adicionam itens enquanto consumidores os removem, com mecanismos de espera e notificação para lidar com situações de estoque vazio ou capacidade máxima atingida.