22. Concorrência e Threads em Java
A concorrência é um conceito fundamental na programação moderna, especialmente em Java. Com o aumento do número de processadores e núcleos em sistemas computacionais, torna-se cada vez mais importante saber como executar tarefas simultaneamente para aproveitar ao máximo os recursos de hardware disponíveis. Neste capítulo, vamos explorar os conceitos de concorrência e threads em Java, mergulhando desde o básico até aspectos avançados.
Fundamentos de Threads
Em Java, uma thread é a menor unidade de execução que pode ser agendada pelo sistema operacional. Uma aplicação Java pode ter várias threads rodando simultaneamente, cada uma cuidando de uma parte diferente do trabalho. Isso permite que programas realizem várias tarefas ao mesmo tempo, como responder a eventos de usuário enquanto realizam cálculos em segundo plano.
Para criar uma thread em Java, existem duas maneiras principais:
- Extendendo a classe Thread: Crie uma classe que estenda
java.lang.Thread
e sobrescreva o métodorun()
. Após criar uma instância da sua classe, chame o métodostart()
para executar a thread. - Implementando a interface Runnable: Crie uma classe que implemente a interface
Runnable
e implemente o métodorun()
. Você pode passar uma instância dessa classe para o construtor deThread
e, em seguida, chamarstart()
.
class MinhaThread extends Thread {
public void run() {
// Código a ser executado em uma nova thread
}
}
class MeuRunnable implements Runnable {
public void run() {
// Código a ser executado em uma nova thread
}
}
public class ExemploThread {
public static void main(String[] args) {
MinhaThread mt = new MinhaThread();
mt.start();
Thread t = new Thread(new MeuRunnable());
t.start();
}
}
Gestão de Threads
Gerenciar threads manualmente pode ser complexo e propenso a erros. Java fornece um conjunto de ferramentas para ajudar a gerenciar a execução de threads, incluindo métodos para:
- Sincronização: Palavra-chave
synchronized
e classes no pacotejava.util.concurrent
, comoLocks
,Semaphores
eCountDownLatch
, ajudam a gerenciar o acesso a recursos compartilhados. - Comunicação entre Threads: Métodos como
wait()
,notify()
enotifyAll()
são usados para a comunicação entre threads. - Gerenciamento de estado: Métodos como
interrupt()
,join()
eisAlive()
ajudam a gerenciar o estado e o ciclo de vida das threads.
Problemas de Concorrência
Desenvolver aplicações concorrentes traz desafios únicos, como:
- Deadlock: Ocorre quando duas ou mais threads estão esperando indefinidamente umas pelas outras para liberar recursos, resultando em um impasse.
- Starvation: Acontece quando uma ou mais threads são impedidas de avançar devido à monopolização de recursos por outras threads.
- Condições de corrida: Surgem quando duas ou mais threads acessam um recurso compartilhado simultaneamente e o resultado final depende da ordem de execução das threads.
Para evitar esses problemas, é essencial entender e aplicar técnicas de sincronização e bloqueio adequadas.
Executores e Serviços de Executor
A partir do Java 5, a API de Concorrência introduziu o framework de executores, que simplifica a execução e gerenciamento de threads. Os executores abstraem a criação e gerenciamento de threads, fornecendo um serviço de executor que pode ser usado para executar tarefas de forma assíncrona.
Exemplos de executores incluem:
- ThreadPoolExecutor: Gerencia um pool de threads reutilizáveis.
- ScheduledThreadPoolExecutor: Permite a execução de tarefas com atraso ou periodicamente.
- Executors.newCachedThreadPool: Cria um pool de threads que cria novas threads conforme necessário, mas reutilizará threads previamente construídas quando estiverem disponíveis.
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.execute(new Runnable() {
public void run() {
// Tarefa a ser executada
}
});
executor.shutdown();
Concorrência de Alto Nível
Além dos executores, a API de Concorrência do Java oferece abstrações de alto nível, como:
Future
eCallable
para trabalhar com resultados de tarefas assíncronas.CompletionService
para gerenciar um conjunto de tarefas assíncronas.Concurrent Collections
comoConcurrentHashMap
eBlockingQueue
para trabalhar com coleções em ambientes concorrentes.
Boas Práticas de Concorrência
Para escrever programas concorrentes robustos e eficientes em Java, é importante seguir algumas boas práticas:
- Minimize o escopo de seções críticas usando sincronização apenas onde é estritamente necessário.
- Evite bloqueios de longa duração para reduzir o risco de deadlock e melhorar a responsividade do aplicativo.
- Prefira as abstrações de alto nível da API de Concorrência do Java em vez de gerenciar threads e sincronização manualmente.
- Use coleções concorrentes para gerenciar o acesso a dados compartilhados entre threads.
- Esteja ciente dos problemas comuns de concorrência, como condições de corrida, deadlocks e starvation, e saiba como evitá-los.
Em resumo, a concorrência em Java é um recurso poderoso que, quando usado corretamente, pode levar a aplicativos mais responsivos e eficientes. No entanto, também introduz complexidade e potenciais armadilhas. Com um entendimento sólido dos conceitos e ferramentas fornecidos pela API de Concorrência do Java, os desenvolvedores podem criar programas concorrentes seguros e eficazes.