Interação entre Threads em Java

Existem duas abordagens principais para a comunicação entre threads em Java:

  1. Utilização de synchronized, wait() e notify()
  2. 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.

  1. 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

  2. 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

  3. ………

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();
}
}
}

Tags: java Multithreading Sincronização Concorrência Lock

Publicado em 6-18 19:57