Métodos wait e notify () em Java
1. Introdução
Neste artigo, veremos um dos mecanismos mais fundamentais em Java - a sincronização de thread.
Discutiremos primeiro alguns termos e metodologias essenciais relacionados à concorrência.
E desenvolveremos um aplicativo simples - onde lidaremos com problemas de simultaneidade, com o objetivo de entender melhorwait()enotify().
2. Sincronização de thread em Java
Em um ambiente multithread, vários encadeamentos podem tentar modificar o mesmo recurso. Se os tópicos não forem gerenciados corretamente, isso irá, é claro, levar a problemas de consistência.
2.1. Blocos protegidos em Java
Uma ferramenta que podemos usar para coordenar ações de vários encadeamentos em Java - são blocos protegidos. Esses blocos controlam uma condição específica antes de retomar a execução.
Com isso em mente, usaremos:
-
Object.wait() - para suspender uma discussão
-
Object.notify() – para despertar um tópico
Isso pode ser melhor compreendido a partir do diagrama a seguir, que representa o ciclo de vida de umThread:
Observe que existem muitas maneiras de controlar este ciclo de vida; no entanto, neste artigo, vamos nos concentrar apenas emwait() enotify().
3. O Métodowait()
Simplificando, quando chamamoswait() –, isso força o encadeamento atual a esperar até que outro encadeamento invoquenotify() ounotifyAll() no mesmo objeto.
Para isso, o thread atual deve possuir o monitor do objeto. De acordo comJavadocs, isso pode acontecer quando:
-
executamos o método de instânciasynchronized para o objeto fornecido
-
executamos o corpo de um blocosynchronized no objeto fornecido
-
executando métodossynchronized static para objetos do tipoClass
Observe que apenas um thread ativo pode possuir o monitor de um objeto por vez.
Este métodowait() vem com três assinaturas sobrecarregadas. Vamos dar uma olhada nisso.
3.1. wait()
O métodowait() faz com que a thread atual espere indefinidamente até que outra thread invoquenotify() para este objeto ounotifyAll().
3.2. wait(long timeout)
Usando esse método, podemos especificar um tempo limite após o qual o thread será acordado automaticamente. Um thread pode ser ativado antes de atingir o tempo limite usandonotify() ounotifyAll().
Observe que chamarwait(0) é o mesmo que chamarwait().
3.3. wait(long timeout, int nanos)
Essa é outra assinatura que fornece a mesma funcionalidade, com a única diferença de que podemos fornecer maior precisão.
O período de tempo limite total (em nanossegundos), é calculado como1_000_000*timeout + nanos.
4. notify() enotifyAll()
O métodonotify() é usado para despertar threads que estão esperando por um acesso ao monitor deste objeto.
Existem duas maneiras de notificar threads em espera.
4.1. notify()
Para todos os threads esperando no monitor deste objeto (usando qualquer um dos métodoswait()), o métodonotify() notifica qualquer um deles para acordar arbitrariamente. A escolha exata de qual thread ativar não é determinística e depende da implementação.
Comonotify() ativa um único encadeamento aleatório, ele pode ser usado para implementar o bloqueio mutuamente exclusivo onde os encadeamentos estão realizando tarefas semelhantes, mas na maioria dos casos, seria mais viável implementarnotifyAll().
4.2. notifyAll()
Este método simplesmente desperta todos os threads que estão esperando no monitor deste objeto.
Os segmentos despertados serão concluídos da maneira usual - como qualquer outro segmento.
Mas antes de permitirmos que sua execução continue, sempredefine a quick check for the condition required to proceed with the thread - porque pode haver algumas situações em que o thread foi ativado sem receber uma notificação (este cenário é discutido posteriormente em um exemplo).
5. Problema de sincronização remetente-receptor
Agora que entendemos o básico, vamos passar por um aplicativoSender -Receiver simples - que fará uso dos métodoswait()enotify() para configurar a sincronização entre eles:
-
OSender deve enviar um pacote de dados para oReceiver
-
OReceiver não pode processar o pacote de dados até queSender termine de enviá-lo
-
Da mesma forma, oSender não deve tentar enviar outro pacote, a menos queReceiver já tenha processado o pacote anterior
Vamos primeiro criar a classeData que consiste nos dadospacket que serão enviados deSender aReceiver. Usaremoswait()enotifyAll() para configurar a sincronização entre eles:
public class Data {
private String packet;
// True if receiver should wait
// False if sender should wait
private boolean transfer = true;
public synchronized void send(String packet) {
while (!transfer) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
transfer = false;
this.packet = packet;
notifyAll();
}
public synchronized String receive() {
while (transfer) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
transfer = true;
notifyAll();
return packet;
}
}
Vamos analisar o que está acontecendo aqui:
-
A variávelpacket denota os dados que estão sendo transferidos pela rede
-
Temos uma variávelbooleantransfer – queSendereReceiver usarão para sincronização:
-
Se esta variável fortrue, então oReceiver deve esperar porSender para enviar a mensagem
-
Se forfalse, entãoSender deve esperar porReceiver para receber a mensagem
-
-
OSender usa o métodosend() para enviar dados paraReceiver:
-
Setransfer forfalse,, vamos esperar chamandowait() neste tópico
-
Mas quando étrue, alternamos o status, definimos nossa mensagem e chamamosnotifyAll() para despertar outros threads para especificar que um evento significativo ocorreu e eles podem verificar se podem continuar a execução
-
-
Da mesma forma, oReceiver usará o métodoreceive():
-
Setransfer foi definido comofalse porSender, então apenas ele continuará, caso contrário, chamaremoswait() neste encadeamento
-
Quando a condição é atendida, alternamos o status, notificamos todos os threads em espera para acordar e retornar o pacote de dados que eraReceiver
-
5.1. Por que incluirwait() em um loopwhile?
Uma vez quenotify()enotifyAll() acorda aleatoriamente threads que estão esperando no monitor deste objeto, nem sempre é importante que a condição seja atendida. Às vezes, pode acontecer que o thread seja ativado, mas a condição ainda não esteja satisfeita.
Também podemos definir uma verificação para nos evitar despertares espúrios - onde um segmento pode acordar da espera sem nunca ter recebido uma notificação.
5.2. Por que precisamos sincronizar os métodosend()ereceive()?
Colocamos esses métodos dentro dos métodossynchronized para fornecer bloqueios intrínsecos. Se um thread que chama o métodowait() não possui o bloqueio inerente, um erro será lançado.
Vamos agora criarSender eReceiver e implementar a interfaceRunnable em ambos para que suas instâncias possam ser executadas por um thread.
Vamos primeiro ver comoSender funcionará:
public class Sender implements Runnable {
private Data data;
// standard constructors
public void run() {
String packets[] = {
"First packet",
"Second packet",
"Third packet",
"Fourth packet",
"End"
};
for (String packet : packets) {
data.send(packet);
// Thread.sleep() to mimic heavy server-side processing
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
}
}
Para esteSender:
-
Estamos criando alguns pacotes de dados aleatórios que serão enviados pela rede na matrizpackets[]
-
Para cada pacote, estamos apenas chamandosend()
-
Então, estamos chamandoThread.sleep() com intervalo aleatório para imitar o processamento pesado do lado do servidor
Finalmente, vamos implementar nossoReceiver:
public class Receiver implements Runnable {
private Data load;
// standard constructors
public void run() {
for(String receivedMessage = load.receive();
!"End".equals(receivedMessage);
receivedMessage = load.receive()) {
System.out.println(receivedMessage);
// ...
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
}
}
Aqui, estamos simplesmente chamandoload.receive() no loop até obtermos o último pacote de dados“End”.
Vamos agora ver este aplicativo em ação:
public static void main(String[] args) {
Data data = new Data();
Thread sender = new Thread(new Sender(data));
Thread receiver = new Thread(new Receiver(data));
sender.start();
receiver.start();
}
Receberemos o seguinte resultado:
First packet
Second packet
Third packet
Fourth packet
E aqui estamos -we’ve received all data packets in the right, sequential ordere estabelecemos com sucesso a comunicação correta entre o remetente e o destinatário.
6. Conclusão
Neste artigo, discutimos alguns conceitos básicos de sincronização em Java; mais especificamente, nos concentramos em como podemos usarwait()enotify() para resolver problemas de sincronização interessantes. E, finalmente, passamos por um exemplo de código em que aplicamos esses conceitos na prática.
Antes de encerrarmos aqui, é importante mencionar que todas essas APIs de baixo nível, comowait(),notify()enotifyAll() - são métodos tradicionais que funcionam bem, mas os mecanismos de nível superior são frequentemente mais simples e melhores - como as interfaces nativasLockeCondition do Java (disponíveis no pacotejava.util.concurrent.locks).
Para obter mais informações sobre o pacotejava.util.concurrent, visite nosso artigooverview of the java.util.concurrent, eLockeCondition são abordados emguide to java.util.concurrent.Locks, here.
Como sempre, os trechos de código completos usados neste artigo estão disponíveisover on GitHub.