As threads permitem que inúmeras atividades ocorram simultaneamente.
A programação concorrente é mais difícil do que a programação em uma única thread, pois muitas coisas podem dar errado, e pode ser difícil de reproduzir as falhas. Você não pode evitar a concorrência. Ela é inerente à plataforma e um requisito para se conseguir um bom desempenho dos processadores multicore, que agora estão por toda parte. Este capítulo apresenta conselhos para ajudá-lo a escrever programas concorrentes claros, corretos e bem documentados.
1. Importância da sincronização para dados mutáveis compartilhados
- A sincronização com a palavra-chave synchronized garante:
- Exclusão mútua: Apenas uma thread pode executar um bloco sincronizado por vez.
- Visibilidade: As mudanças feitas por uma thread tornam-se visíveis para outras threads.
2. Problemas sem sincronização
- Sem sincronização, alterações podem não ser visíveis para outras threads.
- Exemplo: Parar uma thread com um campo boolean (sem sincronização)
public class StopThread {
private static boolean stopRequested = false;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
while (!stopRequested) {
// Loop infinito, pois mudanças em stopRequested podem não ser visíveis
}
});
backgroundThread.start();
Thread.sleep(1000);
stopRequested = true; // Pode nunca ser visto pela outra thread
}
}
3. Correção com sincronização
- Sincronizar leitura e escrita do campo para garantir visibilidade:
public class StopThread {
private static boolean stopRequested;
public static synchronized void requestStop() {
stopRequested = true;
}
public static synchronized boolean stopRequested() {
return stopRequested;
}
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
while (!stopRequested()) {
// Loop agora termina corretamente
}
});
backgroundThread.start();
Thread.sleep(1000);
requestStop();
}
}
4. Uso de volatile para comunicação entre threads
- O modificador volatile garante visibilidade sem exclusão mútua:
public class StopThread {
private static volatile boolean stopRequested = false;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
while (!stopRequested) {
// Loop agora termina corretamente com volatile
}
});
backgroundThread.start();
Thread.sleep(1000);
stopRequested = true;
}
}
5. Problemas com operações compostas (exemplo: incremento não atômico)
- O operador ++ não é seguro para threads, pois envolve múltiplas operações:
public class SerialNumberGenerator {
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++; // Pode gerar números repetidos
}
}
Correção com sincronização:
public class SerialNumberGenerator {
private static int nextSerialNumber = 0;
public static synchronized int generateSerialNumber() {
return nextSerialNumber++;
}
}
- Melhoria com AtomicLong para desempenho e segurança:
import java.util.concurrent.atomic.AtomicLong;
public class SerialNumberGenerator {
private static final AtomicLong nextSerialNumber = new AtomicLong();
public static long generateSerialNumber() {
return nextSerialNumber.getAndIncrement();
}
}
6. Princípios gerais
Evitar compartilhamento de dados mutáveis:
- Preferir dados imutáveis sempre que possível.
- Confinar dados mutáveis a uma única thread.
Publicação segura:
Garantir que um objeto compartilhado seja visível para outras threads:
- Utilizando campos final, volatile, ou sincronização explícita.
- Usando coleções concorrentes, como ConcurrentHashMap.
7. Resumo das boas práticas
- Sempre sincronizar leitura e escrita de dados mutáveis compartilhados.
- Usar volatile apenas para comunicação simples entre threads.
- Preferir classes de utilitários seguros para threads, como as do pacote java.util.concurrent.
Com esses exemplos e práticas, é possível criar programas concorrentes que são claros, corretos e fáceis de manter.
Exemplos do livro:
Top comments (0)