O que é uma classe thread-safe?
Uma classe em Java é considerada thread-safe quando pode ser usada por múltiplas threads simultaneamente sem causar condições de corrida ou deixar o estado do objeto inconsistente. Isso garante que, mesmo com acesso concorrente ao mesmo objeto, ele permanecerá em um estado confiável. No entanto, se um objeto nunca for acessado por mais de uma thread ao mesmo tempo, a preocupação com a segurança de threads é desnecessária.
É comum encontrar cenários em que uma classe não é teoricamente thread-safe, mas, por conta do contexto de uso, ela não precisa ser. Nesse caso, não há razão para aplicar medidas adicionais de segurança.
Entendendo o problema: Condições de corrida
Vejamos um exemplo clássico de uma classe que não é thread-safe:
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // Esta operação não é atômica!
}
public void decrement() {
count--; // Também não é atômica!
}
public int getCount() {
return count;
}
}
Os métodos increment
e decrement
não são thread-safe. Isso acontece porque operações como count++
são compostas de três passos:
- Ler o valor atual de
count
; - Adicionar ou subtrair 1;
- Escrever o novo valor em
count
.
Se duas threads executarem increment()
ao mesmo tempo, ambas podem ler o mesmo valor inicial, resultando em um estado incorreto. Por exemplo:
- Thread A lê
count
(0). - Thread B lê
count
(0). - Thread A soma 1 e escreve 1.
- Thread B também soma 1 e escreve 1.
O valor final de count
é 1, enquanto deveria ser 2. Esse é um exemplo de condição de corrida.
Estratégias para criar classes thread-safe
1. Classes sem estado (stateless): Sem estado, sem problema
Se uma classe não possui nenhum estado compartilhado (variáveis de instância), ela é intrinsecamente thread-safe. Não há nada para ser modificado, então não há risco de corrupção.
public class MathHelper {
// Sem campos = sem estado compartilhado
public int add(int a, int b) {
return a + b;
}
public static int multiply(int a, int b) {
return a * b;
}
public double calculateAverage(int[] numbers) {
int sum = 0;
for (int num : numbers) {
sum += num;
}
return numbers.length > 0 ? (double) sum / numbers.length : 0;
}
}
Essa classe é naturalmente thread-safe, pois cada método opera apenas sobre os parâmetros fornecidos.
2. Classes imutáveis: Objetos somente leitura
A imutabilidade é uma maneira eficaz de garantir segurança em ambientes concorrentes. Objetos imutáveis não podem ser modificados após a criação, eliminando o risco de alterações conflitantes.
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
public ImmutablePoint translate(int dx, int dy) {
return new ImmutablePoint(x + dx, y + dy);
}
}
A classe String
é um exemplo clássico de uma classe imutável e thread-safe em Java.
3. Encapsulamento e sincronização
Para classes que precisam de estado mutável, boas práticas de encapsulamento, combinadas com sincronização, podem garantir a segurança.
- Encapsule variáveis de instância:
public class SafeCounter {
private int count;
public synchronized void increment() {
count++;
}
public synchronized void decrement() {
count--;
}
public synchronized int getCount() {
return count;
}
}
O uso de synchronized
garante que apenas uma thread por vez execute o método, prevenindo condições de corrida.
- Use
volatile
para visibilidade:
public class StatusChecker {
private volatile boolean running = true;
public void stop() {
running = false;
}
public void performTask() {
while (running) {
// Executa a tarefa
}
}
}
A palavra-chave volatile
assegura que mudanças feitas por uma thread sejam visíveis imediatamente para outras threads.
4. Aproveite bibliotecas thread-safe
O Java oferece coleções e utilitários thread-safe que simplificam a construção de classes concorrentes:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadSafeUserManager {
private final ConcurrentHashMap<String, User> users = new ConcurrentHashMap<>();
private final AtomicInteger userCount = new AtomicInteger(0);
public void addUser(String id, User user) {
users.put(id, user);
userCount.incrementAndGet();
}
public User getUser(String id) {
return users.get(id);
}
public int getTotalUsers() {
return userCount.get();
}
}
Conclusão
Criar classes thread-safe em Java requer atenção à forma como elas serão usadas em cenários concorrentes. Entre as melhores práticas, destacam-se:
- Evitar estados compartilhados com classes stateless;
- Garantir imutabilidade sempre que possível;
- Usar sincronização para proteger estados mutáveis;
- Aproveitar coleções e utilitários thread-safe;
- Confinar estados a threads individuais.
Escolher a estratégia certa depende das necessidades e requisitos de desempenho da sua aplicação.