Introdução
Então, sabemos que Java tem threads. Você pode ler sobre isso na revisão intitulada Better together: Java and the Thread class. Parte I — Threads de execução . Os threads são necessários para executar o trabalho em paralelo. Isso torna altamente provável que as threads interajam de alguma forma umas com as outras. Vejamos como isso acontece e quais ferramentas básicas temos.
Colheita
Thread.yield() é desconcertante e raramente usado. É descrito de muitas maneiras diferentes na Internet. Incluindo algumas pessoas escrevendo que existe alguma fila de threads, na qual um thread descerá com base nas prioridades do thread. Outras pessoas escrevem que um thread mudará seu status de "Running" para "Runnable" (mesmo que não haja distinção entre esses status, ou seja, Java não os distingue). A realidade é que tudo é muito menos conhecido e ainda mais simples em certo sentido.
yield()
documentação do método. Se você lê-lo, fica claro que oyield()
na verdade, apenas fornece algumas recomendações ao agendador de encadeamento Java de que esse encadeamento pode receber menos tempo de execução. Mas o que realmente acontece, ou seja, se o escalonador age de acordo com a recomendação e o que ele faz em geral, depende da implementação da JVM e do sistema operacional. E pode depender de alguns outros fatores também. Toda a confusão provavelmente se deve ao fato de que o multithreading foi repensado à medida que a linguagem Java se desenvolveu. Leia mais na visão geral aqui: Breve introdução ao Java Thread.yield() .
Dormir
Um thread pode entrar em suspensão durante sua execução. Este é o tipo mais fácil de interação com outros segmentos. O sistema operacional que executa a máquina virtual Java que executa nosso código Java possui seu próprio agendador de encadeamentos . Ele decide qual thread iniciar e quando. Um programador não pode interagir com este escalonador diretamente do código Java, apenas através da JVM. Ele ou ela pode pedir ao escalonador para pausar o thread por um tempo, ou seja, colocá-lo para dormir. Você pode ler mais nestes artigos: Thread.sleep() e How Multithreading works . Você também pode verificar como os threads funcionam nos sistemas operacionais Windows: Internals of Windows Thread . E agora vamos ver com nossos próprios olhos. Salve o seguinte código em um arquivo chamadoHelloWorldApp.java
:
class HelloWorldApp {
public static void main(String []args) {
Runnable task = () -> {
try {
int secToWait = 1000 * 60;
Thread.currentThread().sleep(secToWait);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(task);
thread.start();
}
}
Como você pode ver, temos uma tarefa que espera 60 segundos, após os quais o programa termina. Compilamos usando o comando " javac HelloWorldApp.java
" e depois executamos o programa usando " java HelloWorldApp
". É melhor iniciar o programa em uma janela separada. Por exemplo, no Windows, é assim: start java HelloWorldApp
. Usamos o comando jps para obter o PID (ID do processo), e abrimos a lista de threads com " jvisualvm --openpid pid
: 
try {
TimeUnit.SECONDS.sleep(60);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
Você notou que estamos atendendo InterruptedException
em todos os lugares? Vamos entender o porquê.
Thread.interrupt()
O problema é que, enquanto um thread está esperando/dormindo, alguém pode querer interromper. Neste caso, lidamos com um arquivoInterruptedException
. Este mecanismo foi criado depois que o Thread.stop()
método foi declarado obsoleto, ou seja, desatualizado e indesejável. O motivo era que, quando o stop()
método era chamado, o thread era simplesmente "morto", o que era muito imprevisível. Não podíamos saber quando o encadeamento seria interrompido e não podíamos garantir a consistência dos dados. Imagine que você está gravando dados em um arquivo enquanto o thread é encerrado. Em vez de matar o thread, os criadores do Java decidiram que seria mais lógico dizer a ele que deveria ser interrompido. Como responder a esta informação é uma questão para o próprio tópico decidir. Para obter mais detalhes, leia Por que Thread.stop foi preterido?no site da Oracle. Vejamos um exemplo:
public static void main(String []args) {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
Neste exemplo, não esperaremos 60 segundos. Em vez disso, exibiremos imediatamente "Interrompido". Isso ocorre porque chamamos o interrupt()
método no thread. Este método define um sinalizador interno chamado "status de interrupção". Ou seja, cada thread possui um sinalizador interno que não é acessível diretamente. Mas temos métodos nativos para interagir com esse sinalizador. Mas essa não é a única maneira. Um thread pode estar em execução, não esperando por algo, simplesmente realizando ações. Mas pode antecipar que outros desejarão encerrar seu trabalho em um momento específico. Por exemplo:
public static void main(String []args) {
Runnable task = () -> {
while(!Thread.currentThread().isInterrupted()) {
// Do some work
}
System.out.println("Finished");
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
No exemplo acima, o while
loop será executado até que a thread seja interrompida externamente. Quanto ao isInterrupted
sinalizador, é importante saber que, se capturarmos um InterruptedException
, o sinalizador isInterrupted será redefinido e isInterrupted()
retornará falso. A classe Thread também tem um método estático Thread.interrupted() que se aplica apenas ao thread atual, mas esse método redefine o sinalizador para falso! Leia mais neste capítulo intitulado Interrupção de Thread .
Junte-se (Aguarde outro tópico terminar)
O tipo mais simples de espera é esperar que outro thread termine.public static void main(String []args) throws InterruptedException {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.join();
System.out.println("Finished");
}
Neste exemplo, o novo thread irá dormir 5 segundos. Ao mesmo tempo, o thread principal aguardará até que o thread adormecido acorde e termine seu trabalho. Se você observar o estado do encadeamento no JVisualVM, ele ficará assim: 
join
método é bem simples, pois é apenas um método com código Java que executa wait()
enquanto a thread na qual é chamada estiver ativa. Assim que o thread morre (quando termina seu trabalho), a espera é interrompida. E essa é toda a magia do join()
método. Então, vamos passar para a coisa mais interessante.
Monitor
Multithreading inclui o conceito de um monitor. A palavra monitor vem do inglês por meio do latim do século XVI e significa "um instrumento ou dispositivo usado para observar, verificar ou manter um registro contínuo de um processo". No contexto deste artigo, tentaremos cobrir o básico. Para quem quiser os detalhes, mergulhe nos materiais vinculados. Começamos nossa jornada com a Java Language Specification (JLS): 17.1. Sincronização . Ele diz o seguinte:
lock()
ou liberá-lo com unlock()
. A seguir, encontraremos o tutorial no site da Oracle: Intrinsic Locks and Synchronization. Este tutorial diz que a sincronização do Java é construída em torno de uma entidade interna chamada bloqueio intrínseco ou bloqueio de monitor . Esse bloqueio geralmente é chamado simplesmente de " monitor ". Também vemos novamente que todo objeto em Java tem um bloqueio intrínseco associado a ele. Você pode ler Java-Intrinsic Locks and Synchronization . A seguir será importante entender como um objeto em Java pode ser associado a um monitor. Em Java, cada objeto possui um cabeçalho que armazena metadados internos não disponíveis para o programador a partir do código, mas que a máquina virtual precisa para funcionar corretamente com os objetos. O cabeçalho do objeto inclui uma "palavra de marca", que se parece com isto: 
https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf
public class HelloWorld{
public static void main(String []args){
Object object = new Object();
synchronized(object) {
System.out.println("Hello World");
}
}
}
Aqui, o thread atual (aquele no qual essas linhas de código são executadas) usa a synchronized
palavra-chave para tentar usar o monitor associado aoobject"\
variável para obter/adquirir o bloqueio. Se ninguém mais estiver disputando o monitor (ou seja, ninguém mais estiver executando código sincronizado usando o mesmo objeto), o Java poderá tentar executar uma otimização chamada "bloqueio tendencioso". Uma tag relevante e um registro sobre qual thread possui o bloqueio do monitor são adicionados à palavra de marca no cabeçalho do objeto. Isso reduz a sobrecarga necessária para bloquear um monitor. Se o monitor pertencia anteriormente a outro thread, esse bloqueio não é suficiente. A JVM alterna para o próximo tipo de bloqueio: "bloqueio básico". Ele usa operações de comparação e troca (CAS). Além do mais, a própria palavra de marca do cabeçalho do objeto não armazena mais a palavra de marca, mas sim uma referência ao local onde ela está armazenada, e a tag muda para que a JVM entenda que estamos usando bloqueio básico. Se vários threads competem (disputam) por um monitor (um adquiriu o bloqueio e um segundo está aguardando a liberação do bloqueio), a tag na palavra de marca muda e a palavra de marca agora armazena uma referência ao monitor como um objeto — alguma entidade interna da JVM. Conforme declarado na JDK Enchancement Proposal (JEP), esta situação requer espaço na área de memória Native Heap para armazenar esta entidade. A referência ao local de memória desta entidade interna será armazenada na palavra de marca do cabeçalho do objeto. Assim, um monitor é realmente um mecanismo para sincronizar o acesso a recursos compartilhados entre vários threads. A JVM alterna entre várias implementações desse mecanismo. Portanto, para simplificar, ao falar sobre o monitor, estamos falando de bloqueios. e um segundo está esperando que o bloqueio seja liberado), então a marca na palavra de marca muda e a palavra de marca agora armazena uma referência ao monitor como um objeto — alguma entidade interna da JVM. Conforme declarado na JDK Enchancement Proposal (JEP), esta situação requer espaço na área de memória Native Heap para armazenar esta entidade. A referência ao local de memória desta entidade interna será armazenada na palavra de marca do cabeçalho do objeto. Assim, um monitor é realmente um mecanismo para sincronizar o acesso a recursos compartilhados entre vários threads. A JVM alterna entre várias implementações desse mecanismo. Portanto, para simplificar, ao falar sobre o monitor, estamos falando de bloqueios. e um segundo está esperando que o bloqueio seja liberado), então a marca na palavra de marca muda e a palavra de marca agora armazena uma referência ao monitor como um objeto — alguma entidade interna da JVM. Conforme declarado na JDK Enchancement Proposal (JEP), esta situação requer espaço na área de memória Native Heap para armazenar esta entidade. A referência ao local de memória desta entidade interna será armazenada na palavra de marca do cabeçalho do objeto. Assim, um monitor é realmente um mecanismo para sincronizar o acesso a recursos compartilhados entre vários threads. A JVM alterna entre várias implementações desse mecanismo. Portanto, para simplificar, ao falar sobre o monitor, estamos falando de bloqueios. e a palavra de marca agora armazena uma referência ao monitor como um objeto — alguma entidade interna da JVM. Conforme declarado na JDK Enchancement Proposal (JEP), esta situação requer espaço na área de memória Native Heap para armazenar esta entidade. A referência ao local de memória desta entidade interna será armazenada na palavra de marca do cabeçalho do objeto. Assim, um monitor é realmente um mecanismo para sincronizar o acesso a recursos compartilhados entre vários threads. A JVM alterna entre várias implementações desse mecanismo. Portanto, para simplificar, ao falar sobre o monitor, estamos falando de bloqueios. e a palavra de marca agora armazena uma referência ao monitor como um objeto — alguma entidade interna da JVM. Conforme declarado na JDK Enchancement Proposal (JEP), esta situação requer espaço na área de memória Native Heap para armazenar esta entidade. A referência ao local de memória desta entidade interna será armazenada na palavra de marca do cabeçalho do objeto. Assim, um monitor é realmente um mecanismo para sincronizar o acesso a recursos compartilhados entre vários threads. A JVM alterna entre várias implementações desse mecanismo. Portanto, para simplificar, ao falar sobre o monitor, estamos falando de bloqueios. A referência ao local de memória desta entidade interna será armazenada na palavra de marca do cabeçalho do objeto. Assim, um monitor é realmente um mecanismo para sincronizar o acesso a recursos compartilhados entre vários threads. A JVM alterna entre várias implementações desse mecanismo. Portanto, para simplificar, ao falar sobre o monitor, estamos falando de bloqueios. A referência ao local de memória desta entidade interna será armazenada na palavra de marca do cabeçalho do objeto. Assim, um monitor é realmente um mecanismo para sincronizar o acesso a recursos compartilhados entre vários threads. A JVM alterna entre várias implementações desse mecanismo. Portanto, para simplificar, ao falar sobre o monitor, estamos falando de bloqueios. 
Sincronizado (aguardando bloqueio)
Como vimos anteriormente, o conceito de "bloco sincronizado" (ou "seção crítica") está intimamente relacionado ao conceito de monitor. Dê uma olhada em um exemplo:public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Runnable task = () -> {
synchronized(lock) {
System.out.println("thread");
}
};
Thread th1 = new Thread(task);
th1.start();
synchronized(lock) {
for (int i = 0; i < 8; i++) {
Thread.currentThread().sleep(1000);
System.out.print(" " + i);
}
System.out.println(" ...");
}
}
Aqui, o thread principal primeiro passa o objeto de tarefa para o novo thread e, em seguida, adquire imediatamente o bloqueio e executa uma longa operação com ele (8 segundos). Todo esse tempo, a tarefa fica impossibilitada de prosseguir, pois não pode entrar no synchronized
bloco, pois o bloqueio já foi adquirido. Se o thread não conseguir obter o bloqueio, ele aguardará o monitor. Assim que obtiver o bloqueio, continuará a execução. Quando um thread sai de um monitor, ele libera o bloqueio. No JVisualVM, fica assim: 
th1.getState()
A instrução no loop for retornará BLOCKED , porque enquanto o loop estiver em execução, o lock
monitor do objeto será ocupado pelo main
thread e o th1
thread será bloqueado e não poderá prosseguir até que o bloqueio seja liberado. Além dos blocos sincronizados, um método inteiro pode ser sincronizado. Por exemplo, aqui está um método da HashTable
classe:
public synchronized int size() {
return count;
}
Este método será executado por apenas um thread em um determinado momento. Precisamos mesmo da fechadura? Sim, precisamos disso. No caso de métodos de instância, o objeto "this" (objeto atual) atua como um bloqueio. Há uma discussão interessante sobre este tópico aqui: Existe uma vantagem em usar um Método Sincronizado em vez de um Bloco Sincronizado? . Se o método for estático, o bloqueio não será o objeto "this" (porque não existe um objeto "this" para um método estático), mas sim um objeto Class (por exemplo, ) Integer.class
.
Aguarde (esperando por um monitor). métodos notify() e notifyAll()
A classe Thread tem outro método de espera associado a um monitor. Ao contrário desleep()
e join()
, esse método não pode ser simplesmente chamado. Seu nome é wait()
. O wait
método é chamado no objeto associado ao monitor que queremos esperar. Vejamos um exemplo:
public static void main(String []args) throws InterruptedException {
Object lock = new Object();
// The task object will wait until it is notified via lock
Runnable task = () -> {
synchronized(lock) {
try {
lock.wait();
} catch(InterruptedException e) {
System.out.println("interrupted");
}
}
// After we are notified, we will wait until we can acquire the lock
System.out.println("thread");
};
Thread taskThread = new Thread(task);
taskThread.start();
// We sleep. Then we acquire the lock, notify, and release the lock
Thread.currentThread().sleep(3000);
System.out.println("main");
synchronized(lock) {
lock.notify();
}
}
No JVisualVM, fica assim: 
wait()
e notify()
estão associados a java.lang.Object
. Pode parecer estranho que métodos relacionados a threads estejam na Object
classe. Mas a razão para isso agora se revela. Você deve se lembrar que todo objeto em Java tem um cabeçalho. O cabeçalho contém várias informações de manutenção, incluindo informações sobre o monitor, ou seja, o status da fechadura. Lembre-se, cada objeto, ou instância de uma classe, está associado a uma entidade interna na JVM, chamada de bloqueio intrínseco ou monitor. No exemplo acima, o código do objeto tarefa indica que entramos no bloco sincronizado para o monitor associado ao lock
objeto. Se conseguirmos adquirir o bloqueio para este monitor, entãowait()
é chamado. A thread que executa a tarefa liberará o lock
monitor do objeto, mas entrará na fila de threads que aguardam notificação do lock
monitor do objeto. Essa fila de threads é chamada de WAIT SET, que reflete mais adequadamente seu propósito. Ou seja, é mais um conjunto do que uma fila. O main
thread cria um novo thread com o objeto de tarefa, inicia-o e aguarda 3 segundos. Isso torna altamente provável que o novo encadeamento consiga adquirir o bloqueio antes do main
encadeamento e entrar na fila do monitor. Depois disso, a main
própria thread entra no lock
bloco sincronizado do objeto e realiza a notificação da thread usando o monitor. Depois que a notificação é enviada, o main
thread libera olock
monitor do objeto, e a nova thread, que antes esperava a lock
liberação do monitor do objeto, continua a execução. É possível enviar uma notificação para apenas um thread ( notify()
) ou simultaneamente para todos os threads da fila ( notifyAll()
). Leia mais aqui: Diferença entre notify() e notifyAll() em Java . É importante observar que a ordem de notificação depende de como a JVM é implementada. Leia mais aqui: Como resolver o starvation com notify e notifyAll? . A sincronização pode ser executada sem especificar um objeto. Você pode fazer isso quando um método inteiro é sincronizado em vez de um único bloco de código. Por exemplo, para métodos estáticos, o bloqueio será um objeto Class (obtido via .class
):
public static synchronized void printA() {
System.out.println("A");
}
public static void printB() {
synchronized(HelloWorld.class) {
System.out.println("B");
}
}
Em termos de uso de bloqueios, ambos os métodos são iguais. Se um método não for estático, a sincronização será realizada usando o atual instance
, ou seja, usando this
. A propósito, dissemos anteriormente que você pode usar o getState()
método para obter o status de um thread. Por exemplo, para um thread na fila esperando por um monitor, o status será WAITING ou TIMED_WAITING, se o wait()
método especificar um tempo limite. 
https://stackoverflow.com/questions/36425942/what-is-the-lifecycle-of-thread-in-java
Ciclo de vida do encadeamento
Ao longo de sua vida, o status de um thread muda. Na verdade, essas mudanças compõem o ciclo de vida do thread. Assim que um thread é criado, seu status é NOVO. Nesse estado, o novo encadeamento ainda não está em execução e o planejador de encadeamento Java ainda não sabe nada sobre ele. Para que o agendador de thread aprenda sobre o thread, você deve chamar othread.start()
método. Em seguida, o thread fará a transição para o estado RUNNABLE. A Internet tem muitos diagramas incorretos que distinguem entre os estados "Executável" e "Executando". Mas isso é um erro, porque o Java não distingue entre "pronto para funcionar" (executável) e "trabalhando" (em execução). Quando um thread está ativo, mas não ativo (não executável), ele está em um dos dois estados:
- BLOQUEADO — aguardando para entrar em uma seção crítica, ou seja, um
synchronized
bloco. - WAITING — esperando que outro thread satisfaça alguma condição.
getState()
método. Threads também possuem um isAlive()
método, que retorna true se o thread não for TERMINADO.
LockSupport e thread parking
A partir do Java 1.6, surgiu um mecanismo interessante chamado LockSupport .
park()
método retorna imediatamente se a permissão estiver disponível, consumindo a permissão no processo. Caso contrário, bloqueia. Chamar o unpark
método torna a permissão disponível se ela ainda não estiver disponível. Existe apenas 1 autorização. A documentação do Java para LockSupport
refere-se à Semaphore
classe. Vejamos um exemplo simples:
import java.util.concurrent.Semaphore;
public class HelloWorldApp{
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(0);
try {
semaphore.acquire();
} catch (InterruptedException e) {
// Request the permit and wait until we get it
e.printStackTrace();
}
System.out.println("Hello, World!");
}
}
Este código sempre vai esperar, pois agora o semáforo tem 0 permissões. E quando acquire()
é chamado no código (ou seja, solicita a permissão), a thread espera até receber a permissão. Como estamos esperando, devemos manipular InterruptedException
. Curiosamente, o semáforo obtém um estado de thread separado. Se olharmos no JVisualVM, veremos que o estado não é "Wait", mas sim "Park". 
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
// Park the current thread
System.err.println("Will be Parked");
LockSupport.park();
// As soon as we are unparked, we will start to act
System.err.println("Unparked");
};
Thread th = new Thread(task);
th.start();
Thread.currentThread().sleep(2000);
System.err.println("Thread state: " + th.getState());
LockSupport.unpark(th);
Thread.currentThread().sleep(2000);
}
O status da thread será WAITING, mas JVisualVM distingue entre wait
da synchronized
palavra-chave e park
da LockSupport
classe. Por que isso é LockSupport
tão importante? Voltamos novamente para a documentação do Java e examinamos o estado do encadeamento WAITING . Como você pode ver, existem apenas três maneiras de entrar nele. Duas dessas maneiras são wait()
e join()
. E o terceiro é LockSupport
. Em Java, os bloqueios também podem ser construídos em LockSuppor
t e oferecer ferramentas de nível superior. Vamos tentar usar um. Por exemplo, dê uma olhada em ReentrantLock
:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HelloWorld{
public static void main(String []args) throws InterruptedException {
Lock lock = new ReentrantLock();
Runnable task = () -> {
lock.lock();
System.out.println("Thread");
lock.unlock();
};
lock.lock();
Thread th = new Thread(task);
th.start();
System.out.println("main");
Thread.currentThread().sleep(2000);
lock.unlock();
}
}
Assim como nos exemplos anteriores, tudo é simples aqui. O lock
objeto espera que alguém libere o recurso compartilhado. Se olharmos no JVisualVM, veremos que a nova thread ficará estacionada até que a main
thread libere o bloqueio para ela. Você pode ler mais sobre bloqueios aqui: Java 8 StampedLocks vs. ReadWriteLocks e Synchronized and Lock API in Java. Para entender melhor como os bloqueios são implementados, é útil ler sobre o Phaser neste artigo: Guide to the Java Phaser . E falando sobre vários sincronizadores, você deve ler o artigo DZone em The Java Synchronizers.
Conclusão
Nesta revisão, examinamos as principais maneiras pelas quais os encadeamentos interagem em Java. Material adicional:- Melhor juntos: Java e a classe Thread. Parte I — Threads de execução
- https://dzone.com/articles/the-java-synchronizers
- https://www.javatpoint.com/java-multithreading-interview-questions
GO TO FULL VERSION