Existem duas abordagens principais para a comunicação entre threads em Java:
- Utilização de synchronized, wait() e notify()
- Utilização de Lock e Condition
Sincronização com wait() e notify()
O método wait() coloca a thread atual em estado de espera, aguardando que outra thread invoque notify() ou notifyAll() para despertá-la.
Os métodos wait(), notify(), notifyAll(), wait(long) e wait(long, int) devem ser chamados apenas quando o objeto está bloqueado com synchronized, caso contrário, será lançada uma exceção java.lang.IllegalMonitorStateException. Por exemplo:
synchronized(valor){
valor.notifyAll();
}
public synchronized void metodo(){
this.notifyAll();
}
public static synchronized void metodoEstatico() {
MinhaClasse.class.notifyAll();
}
Problemas com wait(): A documentação desses métodos menciona que interrupções e despertares falsos são possíveis, portanto, esses métodos devem sempre ser usados dentro de um loop:
Assim como na versão de um argumento, interrupções e despertares falsos são possíveis, e este método deve sempre ser usado em um loop:
synchronized (obj) { while (<condição não atendida>) obj.wait(); // Executa ação apropriada para a condição }
class FabricanteBolos{
private int quantidadeBolos = 0;
/**
* Não produz bolos enquanto houver bolos não consumidos.
*/
public synchronized void produzir() {
if(quantidadeBolos != 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
quantidadeBolos++;
System.out.println(Thread.currentThread().getName()+" #####produziu \t"+quantidadeBolos);
this.notifyAll();
}
/**
* Não consome bolos enquanto não houver bolos produzidos.
*/
public synchronized void consumir(){
if(quantidadeBolos != 1){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
quantidadeBolos--;
System.out.println(Thread.currentThread().getName()+" consumiu \t"+quantidadeBolos);
this.notifyAll();
}
}
Analisando a lógica do código acima: as threads A e B são responsáveis por produzir bolos em ciclo, enquanto a thread C é responsável por consumir bolos em ciclo.
-
A => adquire o bloqueio, quantidadeBolos é 0, produz, quantidadeBolos+1, notifyAll, segunda iteração, quantidadeBolos é 1, wait, libera bloqueio, para na linha 10
B => aguarda o bloqueio de A => adquire bloqueio, quantidadeBolos é 1, wait, libera bloqueio, para na linha 10
C => aguarda bloqueio de A => aguarda bloqueio de B => adquire bloqueio, quantidadeBolos é 1, consome, quantidadeBolos-1, notifyAll, segunda iteração, quantidadeBolos é 0, wait, libera bloqueio, para na linha 26
-
A e B disputam o bloqueio, suponha que A obtenha primeiro
A => adquire bloqueio, continua da linha 10, produz, quantidadeBolos+1, notifyAll, terceira iteração, quantidadeBolos é 1, wait, libera bloqueio, para na linha 10
B => aguarda bloqueio de A => adquire bloqueio, continua da linha 10, produz, quantidadeBolos+1, notifyAll, segunda iteração, quantidadeBolos é 2, wait, libera bloqueio, para na linha 10
C => aguarda bloqueio de A => aguarda bloqueio de B => adquire bloqueio, continua da linha 26, consome, quantidadeBolos-1, notifyAll, terceira iteração, quantidadeBolos é 1, consome, quantidadeBolos-1, notifyAll, quarta iteração, quantidadeBolos é 0, wait, libera bloqueio, para na linha 26
-
………
O comportamento desejado seria produzir um bolo de cada vez, mas o código acima produziu dois bolos consecutivamente. Segundo a documentação, substituir if por while resolve o problema. Vamos analisar novamente.
Utilização de Lock e Condition
Esta abordagem oferece mais flexibilidade em comparação com a primeira.
Segundo a documentação da interface Condition:
A interface Condition separa os métodos do monitor de objeto (wait, notify e notifyAll) em objetos distintos, proporcionando o efeito de ter múltiplas filas de espera por objeto, combinando-os com o uso de implementações arbitrárias de Lock. Onde um Lock substitui o uso de métodos e blocos synchronized, uma Condition substitui o uso dos métodos do monitor de objeto.
Em termos simples, quando usamos Lock em vez de synchronized, precisamos usar Condition para substituir wait(), notify() e outros métodos. Diferente de wait() e notify(), uma Condition pode adicionar múltiplas condições a um objeto bloqueado. Quando o objeto bloqueado é o mesmo, notifyAll() desperta todas as threads em espera e notify() desperta apenas uma thread aleatória. Com Condition, quando o objeto bloqueado é o mesmo, podemos ter threads esperando porque a condição 1 não foi atendida e outras esperando porque a condição 2 não foi atendida... Quando a condição 1 é satisfeita, podemos despertar apenas as threads que esperavam pela condição 1, enquanto as outras continuam esperando.
Exemplo da documentação da interface:
Um buffer com limite, que quando está cheio, a operação de put() entra em estado de espera até que haja espaço disponível. Quando o buffer está vazio, a operação take() entra em estado de espera até que haja dados disponíveis no buffer.
Neste exemplo, são usadas duas condições para limitar as threads de put() e take().
clas BufferDelimitado {
final Lock lock = new ReentrantLock();
final Condition naoCheio = lock.newCondition();
// Quando a condição de buffer não cheio é atednida, desperta as threads de put(), quando não é atendida, coloca as threads de put() em espera.
final Condition naoVazio = lock.newCondition();
final Object[] itens = new Object[100];
int indiceInsercao, indiceRemocao, contador;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
// Verifica se o buffer está cheio
while (contador == itens.length)
// Se estiver cheio, a condição naoCheio não é atendida, então a thread é colocada em espera
naoCheio.await();
itens[indiceInsercao] = x;
if (++indiceInsercao == itens.length) indiceInsercao = 0;
++contador;
// Após inserir dados, o buffer não está mais vazio, então desperta as threads de take() que estavam esperando por buffer não vazio
naoVazio.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
// Verifica se o buffer está vazio
while (contador == 0)
// Se estiver vazio, a condição naoVazio não é atendida, então a thread é colocada em espera
naoVazio.await();
Object x = itens[indiceRemocao];
if (++indiceRemocao == itens.length) indiceRemocao = 0;
--contador;
// Após remover dados, o buffer não está mais cheio, então desperta as threads de put() que estavam esperando por buffer não cheio
naoCheio.signal();
return x;
} finally {
lock.unlock();
}
}
}