Uma breve visão geral dos detalhes de como as threads interagem. Anteriormente, vimos como os threads são sincronizados entre si. Desta vez, vamos nos aprofundar nos problemas que podem surgir à medida que os threads interagem e falaremos sobre como evitá-los. Também forneceremos alguns links úteis para um estudo mais aprofundado.
Com um plug-in JVisualVM instalado (via Ferramentas -> Plugins), podemos ver onde ocorreu o impasse:
De acordo com o JVisualVM, vemos períodos de hibernação e um período de estacionamento (é quando um thread tenta adquirir um bloqueio — ele entra no estado de park, como discutimos anteriormente quando falamos sobre sincronização de thread ) . Você pode ver um exemplo de livelock aqui: Java - Thread Livelock .
Você pode ver um super exemplo aqui: Java - Thread Starvation and Fairness . Este exemplo mostra o que acontece com os encadeamentos durante a fome e como uma pequena alteração de
Esta inspeção foi adicionada ao IntelliJ IDEA como parte do problema IDEA-61117 , listado nas Notas de versão em 2010.

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 . E exploramos o fato de que os encadeamentos podem sincronizar uns com os outros na revisão intitulada Melhor juntos: Java e a classe Thread. Parte II — Sincronização . É hora de falar sobre como os threads interagem uns com os outros. Como eles compartilham recursos compartilhados? Que problemas podem surgir aqui?
Impasse
O problema mais assustador de todos é o impasse. Deadlock é quando duas ou mais threads estão eternamente esperando uma pela outra. Vamos pegar um exemplo da página da Oracle que descreve o impasse :public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
new Thread(() -> alphonse.bow(gaston)).start();
new Thread(() -> gaston.bow(alphonse)).start();
}
}
O impasse pode não ocorrer aqui na primeira vez, mas se o seu programa travar, é hora de executar jvisualvm
: 
"Thread-1" - Thread t@12
java.lang.Thread.State: BLOCKED
at Deadlock$Friend.bowBack(Deadlock.java:16)
- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
O thread 1 está esperando o bloqueio do thread 0. Por que isso acontece? Thread-1
começa a correr e executa o Friend#bow
método. Está marcado com a synchronized
palavra-chave, o que significa que estamos adquirindo o monitor para this
(o objeto atual). A entrada do método era uma referência ao outro Friend
objeto. Agora, Thread-1
deseja executar o método no outro Friend
, e deve adquirir seu bloqueio para fazê-lo. Mas se a outra thread (neste caso Thread-0
) conseguiu entrar no bow()
método, então o bloqueio já foi adquirido e Thread-1
aguardaThread-0
, e vice versa. Esse impasse é insolúvel e o chamamos de impasse. Como um aperto mortal que não pode ser liberado, o impasse é um bloqueio mútuo que não pode ser quebrado. Para outra explicação sobre deadlock, você pode assistir a este vídeo: Deadlock e Livelock explicados .
Livelock
Se houver deadlock, também haverá livelock? Sim, existe :) O Livelock acontece quando os threads externamente parecem estar vivos, mas são incapazes de fazer qualquer coisa, porque a(s) condição(ões) necessária(s) para que continuem seu trabalho não pode(m) ser cumprida(s). Basicamente, o livelock é semelhante ao deadlock, mas os threads não "travam" esperando por um monitor. Em vez disso, eles estão sempre fazendo algo. Por exemplo:import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class App {
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_PURPLE = "\u001B[35m";
public static void log(String text) {
String name = Thread.currentThread().getName(); // Like "Thread-1" or "Thread-0"
String color = ANSI_BLUE;
int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
if (val != 0) {
color = ANSI_PURPLE;
}
System.out.println(color + name + ": " + text + color);
try {
System.out.println(color + name + ": wait for " + val + " sec" + color);
Thread.currentThread().sleep(val * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Lock first = new ReentrantLock();
Lock second = new ReentrantLock();
Runnable locker = () -> {
boolean firstLocked = false;
boolean secondLocked = false;
try {
while (!firstLocked || !secondLocked) {
firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
log("First Locked: " + firstLocked);
secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
log("Second Locked: " + secondLocked);
}
first.unlock();
second.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(locker).start();
new Thread(locker).start();
}
}
O sucesso desse código depende da ordem na qual o planejador de encadeamento Java inicia os encadeamentos. Se Thead-1
começar primeiro, teremos livelock:
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
Como você pode ver no exemplo, ambos os encadeamentos tentam adquirir os dois bloqueios por vez, mas eles falham. Mas, eles não estão em um impasse. Externamente, está tudo bem e eles estão fazendo seu trabalho. 
Inanição
Além do deadlock e do livelock, há outro problema que pode ocorrer durante o multithreading: starvation. Esse fenômeno difere das formas anteriores de bloqueio porque os encadeamentos não são bloqueados — eles simplesmente não têm recursos suficientes. Como resultado, enquanto algumas threads levam todo o tempo de execução, outras não conseguem executar:
https://www.logicbig.com/
Thread.sleep()
para Thread.wait()
permite distribuir a carga uniformemente. 
Condições da corrida
Em multithreading, existe uma "condição de corrida". Esse fenômeno ocorre quando as threads compartilham um recurso, mas o código é escrito de forma a não garantir o compartilhamento correto. Dê uma olhada em um exemplo:public class App {
public static int value = 0;
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
int oldValue = value;
int newValue = ++value;
if (oldValue + 1 != newValue) {
throw new IllegalStateException(oldValue + " + 1 = " + newValue);
}
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
Este código pode não gerar um erro na primeira vez. Quando isso acontecer, pode ser assim:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
at App.lambda$main$0(App.java:13)
at java.lang.Thread.run(Thread.java:745)
Como você pode ver, algo deu errado enquanto newValue
estava sendo atribuído um valor. newValue
é muito grande. Por causa da condição de corrida, um dos threads conseguiu alterar as variáveis value
entre as duas instruções. Acontece que há uma corrida entre os fios. Agora pense em como é importante não cometer erros semelhantes com transações monetárias... Exemplos e diagramas também podem ser vistos aqui: Código para simular condição de corrida em thread Java .
Volátil
Falando sobre a interação de threads,volatile
vale a pena mencionar a palavra-chave. Vejamos um exemplo simples:
public class App {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Runnable whileFlagFalse = () -> {
while(!flag) {
}
System.out.println("Flag is now TRUE");
};
new Thread(whileFlagFalse).start();
Thread.sleep(1000);
flag = true;
}
}
O mais interessante é que é altamente provável que isso não funcione. O novo thread não verá a alteração no flag
campo. Para corrigir isso para o flag
campo, precisamos usar a volatile
palavra-chave. Como e por quê? O processador executa todas as ações. Mas os resultados dos cálculos devem ser armazenados em algum lugar. Para isso existe a memória principal e existe o cache do processador. Os caches de um processador são como um pequeno pedaço de memória usado para acessar dados mais rapidamente do que ao acessar a memória principal. Mas tudo tem um lado negativo: os dados no cache podem não estar atualizados (como no exemplo acima, quando o valor do campo flag não foi atualizado). Então ovolatile
A palavra-chave informa à JVM que não queremos armazenar em cache nossa variável. Isso permite que o resultado atualizado seja visto em todos os encadeamentos. Esta é uma explicação altamente simplificada. Quanto à volatile
palavra-chave, recomendo fortemente que você leia este artigo . Para mais informações, também aconselho você a ler Java Memory Model e Java Volatile Keyword . Além disso, é importante lembrar que volatile
se trata da visibilidade, e não da atomicidade das mudanças. Observando o código na seção "Condições de corrida", veremos uma dica de ferramenta no IntelliJ IDEA: 

Atomicidade
Operações atômicas são operações que não podem ser divididas. Por exemplo, a operação de atribuir um valor a uma variável deve ser atômica. Infelizmente, a operação de incremento não é atômica, porque o incremento requer até três operações de CPU: obtenha o valor antigo, adicione um a ele e salve o valor. Por que a atomicidade é importante? Com a operação de incremento, se houver uma condição de corrida, o recurso compartilhado (ou seja, o valor compartilhado) pode mudar repentinamente a qualquer momento. Além disso, operações envolvendo estruturas de 64 bits, por exemplolong
e double
, não são atômicas. Mais detalhes podem ser lidos aqui: Garantir atomicidade ao ler e escrever valores de 64 bits . Problemas relacionados à atomicidade podem ser vistos neste exemplo:
public class App {
public static int value = 0;
public static AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
value++;
atomic.incrementAndGet();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
Thread.sleep(300);
System.out.println(value);
System.out.println(atomic.get());
}
}
A classe especial AtomicInteger
sempre nos dará 30.000, mas mudará value
de tempos em tempos. Há uma breve visão geral deste tópico: Introdução às variáveis atômicas em Java . O algoritmo "comparar e trocar" está no centro das classes atômicas. Você pode ler mais sobre isso aqui em Comparação de algoritmos sem bloqueio - CAS e FAA no exemplo de JDK 7 e 8 ou no artigo Compare-and-swap na Wikipedia. 
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
acontece antes
Existe um conceito interessante e misterioso chamado "acontece antes". Como parte de seu estudo de threads, você deve ler sobre isso. O relacionamento que acontece antes mostra a ordem na qual as ações entre as threads serão vistas. Existem muitas interpretações e comentários. Aqui está uma das apresentações mais recentes sobre este assunto: Java "Happens-Before" Relationships .Resumo
Nesta revisão, exploramos algumas das especificidades de como os encadeamentos interagem. Discutimos problemas que podem surgir, bem como formas de identificá-los e eliminá-los. Lista de materiais adicionais sobre o tema:- Bloqueio duplamente verificado
- Perguntas frequentes sobre JSR 133 (modelo de memória Java)
- QI 35: Como evitar um impasse?
- Conceitos de simultaneidade em Java por Douglas Hawkins (2017)
GO TO FULL VERSION