A JVM pode assumir que outros encadeamentos não alteram a pizzaArrived
variável durante o loop. Em outras palavras, ele pode içar o pizzaArrived == false
teste fora do circuito, otimizando isso:
while (pizzaArrived == false) {}
nisso:
if (pizzaArrived == false) while (true) {}
que é um loop infinito.
Para garantir que as alterações feitas por um thread sejam visíveis para outros threads, você deve sempre adicionar alguma sincronização entre os threads. A maneira mais simples de fazer isso é tornar a variável compartilhada volatile
:
volatile boolean pizzaArrived = false;
Criar uma variável volatile
garante que diferentes threads verão os efeitos das alterações uns dos outros. Isso evita que a JVM armazene em cache o valor pizzaArrived
ou eleve o teste fora do loop. Em vez disso, ele deve ler o valor da variável real todas as vezes.
(Mais formalmente, volatile
cria uma relação acontece-antes entre os acessos à variável. Isso significa que todos os outros trabalhos que um encadeamento fez antes de entregar a pizza também são visíveis para o encadeamento que recebe a pizza, mesmo que essas outras alterações não sejam nas volatile
variáveis.)
Os métodos sincronizados são usados principalmente para implementar a exclusão mútua (evitando que duas coisas aconteçam ao mesmo tempo), mas também têm todos os mesmos efeitos colaterais volatile
. Usá-los ao ler e escrever uma variável é outra maneira de tornar as alterações visíveis para outros threads:
class MyHouse {
boolean pizzaArrived = false;
void eatPizza() {
while (getPizzaArrived() == false) {}
System.out.println("That was delicious!");
}
synchronized boolean getPizzaArrived() {
return pizzaArrived;
}
synchronized void deliverPizza() {
pizzaArrived = true;
}
}
O efeito de uma declaração impressa
System.out
é um PrintStream
objeto. Os métodos de PrintStream
são sincronizados assim:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
A sincronização evita o pizzaArrived
armazenamento em cache durante o loop. A rigor, os dois threads devem ser sincronizados no mesmo objeto para garantir que as alterações na variável sejam visíveis. (Por exemplo, chamar println
após a configuração pizzaArrived
e chamá-lo novamente antes da leitura pizzaArrived
seria correto.) Se apenas um encadeamento sincroniza em um objeto específico, o JVM tem permissão para ignorá-lo. Na prática, a JVM não é inteligente o suficiente para provar que outros encadeamentos não chamarão println
após a configuração pizzaArrived
, portanto, presume que sim. Portanto, ele não pode armazenar em cache a variável durante o loop se você chamar System.out.println
. É por isso que loops como este funcionam quando têm uma instrução de impressão, embora não seja uma correção correta.
Usar System.out
não é a única maneira de causar esse efeito, mas é a que as pessoas descobrem com mais frequência, quando estão tentando depurar por que o loop não funciona!
O maior problema
while (pizzaArrived == false) {}
é um loop de espera ocupada. Isso é ruim! Enquanto espera, ele consome a CPU, o que desacelera outros aplicativos e aumenta o uso de energia, a temperatura e a velocidade do ventilador do sistema. Idealmente, gostaríamos que o thread de loop dormisse enquanto espera, para que não monopolize a CPU.
Aqui estão algumas maneiras de fazer isso:
Usando esperar / notificar
Uma solução de baixo nível é usar os métodos de espera / notificação deObject
:
class MyHouse {
boolean pizzaArrived = false;
void eatPizza() {
synchronized (this) {
while (!pizzaArrived) {
try {
this.wait();
} catch (InterruptedException e) {}
}
}
System.out.println("That was delicious!");
}
void deliverPizza() {
synchronized (this) {
pizzaArrived = true;
this.notifyAll();
}
}
}
Nesta versão do código, o thread de loop chama wait()
, o que coloca o thread em suspensão. Ele não usará nenhum ciclo de CPU durante o sono. Depois que o segundo encadeamento define a variável, ele chama notifyAll()
para acordar qualquer / todos os encadeamentos que estavam esperando por esse objeto. É como ter o entregador da pizza tocando a campainha, para que você possa sentar e descansar enquanto espera, em vez de ficar parado sem jeito na porta.
Ao chamar esperar / notificar em um objeto, você deve manter o bloqueio de sincronização desse objeto, que é o que o código acima faz. Você pode usar qualquer objeto que desejar, desde que ambos os threads usem o mesmo objeto: aqui eu usei this
(a instância de MyHouse
). Normalmente, dois threads não seriam capazes de inserir blocos sincronizados do mesmo objeto simultaneamente (o que é parte do propósito da sincronização), mas funciona aqui porque um thread libera temporariamente o bloqueio de sincronização quando está dentro do wait()
método.
BlockingQueue
A BlockingQueue
é usado para implementar filas produtor-consumidor. Os "consumidores" pegam os itens da frente da fila e os "produtores" os empurram para trás. Um exemplo:
class MyHouse {
final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();
void eatFood() throws InterruptedException {
// take next item from the queue (sleeps while waiting)
Object food = queue.take();
// and do something with it
System.out.println("Eating: " + food);
}
void deliverPizza() throws InterruptedException {
// in producer threads, we push items on to the queue.
// if there is space in the queue we can return immediately;
// the consumer thread(s) will get to it later
queue.put("A delicious pizza");
}
}
Nota: Os métodos put
e take
de BlockingQueue
podem lançar InterruptedException
s, que são exceções verificadas que devem ser tratadas. No código acima, para simplificar, as exceções são relançadas. Você pode preferir capturar as exceções nos métodos e tentar novamente a chamada put ou take para ter certeza de que terá êxito. Tirando aquele ponto de feiura, BlockingQueue
é muito fácil de usar.
Nenhuma outra sincronização é necessária aqui porque um BlockingQueue
garante que tudo o que os threads fizeram antes de colocar itens na fila esteja visível para os threads que estão retirando esses itens.
Executores
Executor
s são como s prontos para BlockingQueue
executar tarefas. Exemplo:
// A "SingleThreadExecutor" has one work thread and an unlimited queue
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };
// we submit tasks which will be executed on the work thread
executor.execute(eatPizza);
executor.execute(cleanUp);
// we continue immediately without needing to wait for the tasks to finish
Para mais detalhes ver o doc para Executor
, ExecutorService
, e Executors
.
Manipulação de eventos
O loop enquanto espera o usuário clicar em algo em uma IU está errado. Em vez disso, use os recursos de manipulação de eventos do kit de ferramentas de UI. No Swing , por exemplo:
JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
// This event listener is run when the button is clicked.
// We don't need to loop while waiting.
label.setText("Button was clicked");
});
Como o manipulador de eventos é executado no thread de despacho de eventos, um longo trabalho no manipulador de eventos bloqueia outra interação com a IU até que o trabalho seja concluído. Operações lentas podem ser iniciadas em um novo encadeamento ou despachadas para um encadeamento em espera usando uma das técnicas acima (esperar / notificar, a BlockingQueue
ou Executor
). Você também pode usar um SwingWorker
, que é projetado exatamente para isso e fornece automaticamente um thread de trabalho em segundo plano:
JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");
// Add a click listener for the button
button.addActionListener((ActionEvent e) -> {
// Defines MyWorker as a SwingWorker whose result type is String:
class MyWorker extends SwingWorker<String,Void> {
@Override
public String doInBackground() throws Exception {
// This method is called on a background thread.
// You can do long work here without blocking the UI.
// This is just an example:
Thread.sleep(5000);
return "Answer is 42";
}
@Override
protected void done() {
// This method is called on the Swing thread once the work is done
String result;
try {
result = get();
} catch (Exception e) {
throw new RuntimeException(e);
}
label.setText(result); // will display "Answer is 42"
}
}
// Start the worker
new MyWorker().execute();
});
Cronômetros
Para realizar ações periódicas, você pode usar a java.util.Timer
. É mais fácil de usar do que escrever seu próprio loop de tempo e mais fácil de iniciar e parar. Esta demonstração imprime a hora atual uma vez por segundo:
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
};
timer.scheduleAtFixedRate(task, 0, 1000);
Cada um java.util.Timer
tem seu próprio thread de segundo plano que é usado para executar seus programas programados TimerTask
. Naturalmente, o thread dorme entre as tarefas, portanto, não monopoliza a CPU.
No código Swing, há também um javax.swing.Timer
, que é semelhante, mas executa o ouvinte no encadeamento Swing, para que você possa interagir com segurança com os componentes Swing sem precisar alternar os encadeamentos manualmente:
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(1000, (ActionEvent e) -> {
frame.setTitle(String.valueOf(System.currentTimeMillis()));
});
timer.setRepeats(true);
timer.start();
frame.setVisible(true);
Outras maneiras
Se você estiver escrevendo código multithread, vale a pena explorar as classes desses pacotes para ver o que está disponível:
E também consulte a seção Simultaneidade dos tutoriais Java. O multithreading é complicado, mas há muita ajuda disponível!