Em aplicações Java que processam múltiplas requisições simultâneas, a manipulação de coleções compartilhadas entre threads pode resultar em condições de corrida e comportamentos inesperados. Este artigo explora as principais alternativas para garantir a segurança em operações com listas.1. O Problema com Coleções Não-Síncronas
1.1 - Demonstração do Problema
Considere o seguinte cenário onde múltiplas threads acessam uma lista compartilhada:
public class SegurancaColecaoDemo {
public static void main(String[] args) {
List<String> dados = new ArrayList<>();
for (int i = 0; i < 50; i++) {
new Thread(() -> {
dados.add(UUID.randomUUID().toString());
System.out.println(dados);
}, "Thread-" + i).start();
}
}
}
Exceção resultante:java.util.ConcurrentModificationException
**Por que ocorre este erro?**A classe ArrayList não implementa nenhum mecanismo de sincronização interna. Quando várias threads tentam modificar a estrutura simultaneamente, o iterador interno pode detectar uma modificação concorrente e lançar a exceção.
Quais são as soluções disponíveis para garantir a segurança em operações de lista?
1.2 - Vector: Uma Abordagem Tardicional
A classe Vector está presente desde o JDK 1.0 e implementa a interface List. Esta classe utiliza métodos sincronizados internamente, garantindo que apenas uma thread possa executar operações de modificação por vez.
Principais características:
- Herda de
AbstractListe implementaList,RandomAccess,Cloneable - Oferece acesso aleatório eficiente através da interface
RandomAccess - Todos os métodos são sincronizados, garantindo segurança em ambiente multi-thread
Exemplo corrigido utilizando Vector:
public class SegurancaColecaoDemo {
public static void main(String[] args) {
List<String> dados = new Vector<>();
for (int i = 0; i < 50; i++) {
new Thread(() -> {
dados.add(UUID.randomUUID().toString());
System.out.println(dados);
}, "Thread-" + i).start();
}
}
}
**Agora não ocorre exceção. Por quê?**O método add() em Vector possui a palavra-chave synchronized, garantindo exclusão mútua e prevenindo condições de corrida.
1.3 - Collections.synchronizedList()
A classe Collections fornece métodos utilitários para criar versões sincronizadas de coleções. O método synchronizedList() envolve uma lista existente com um wrapper que utiliza sincronização.
Exemplo de implementação:
public class SegurancaColecaoDemo {
public static void main(String[] args) {
List<String> dados = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 50; i++) {
new Thread(() -> {
dados.add(UUID.randomUUID().toString());
System.out.println(dados);
}, "Thread-" + i).start();
}
}
}
Não ocorre exceção de modificação concorrente, pois todas as operações passam pelo wrapper sincronizado.
1.4 - CopyOnWriteArrayList: Leitura e Escrita Separadas
A classe CopyOnWriteArrayList é uma implementação thread-safe que utiliza uma estratégia de cópia em gravação.
Características principais:
- Ideal para cenários onde a lista é pequena e operações de leitura predominam sobre modificações
- Implementa operações de leitura sem necessidade de bloqueio
- Operações de escrita (add, set, remove) criam uma nova cópia do array interno
- Iteradores trabalham com快照 (snapshot) do array no momento da criação
**Princípio de funcionamento:**Quando uma thread precisa adicionar elementos, o array existente não é modificado diretamente. Em vez disso, uma cópia é criada, a modificação ocorre no novo array, e então a referência é atualizada. Desta forma, leitores podem acessar o array sem bloqueios.
**Ponto de atenção:**Esta abordagem pode introduzir inconsistência de dados se uma thread estiver lendo enquanto outra escribe. A estratégia de "copiar tudo" pode impactar performance em operações de escrita frequentes.
Exemplo de utilização:
public class SegurancaColecaoDemo {
public static void main(String[] args) {
List<String> dados = new CopyOnWriteArrayList<>();
for (int i = 0; i < 50; i++) {
new Thread(() -> {
dados.add(UUID.randomUUID().toString());
System.out.println(dados);
}, "Thread-" + i).start();
}
}
}
Análise técnica (essencial):
- Mecanismo de array dinâmico:
- Armazena dados em um array volátil interno
- Operações de modificação criam uma nova cópia do array
- Leituras são rápidas, mas escritas possuem custo elevado devido à replicação
- Mecanismo de segurança em thread:
- Utiliza
volatilepara garantir visibilidade entre threads - emprega bloqueios (locks) para operações de escrita
- Garante que leituras sempre veem a versão mais recente dos dados
1.5 - Resumo das Soluções
- Coleções tradicionais vs thread-safe:
- ArrayList → Vector
- HashMap → Hashtable
- Ambas as abordagens utilizam
synchronizedpara garantir segurança - Coleções wrapper via Collections:
Collections.synchronizedList(),Collections.synchronizedMap(), etc.- Abstração sobre coleções não-síncronas
- Pacote java.util.concurrent:
CopyOnWriteArrayListeCopyOnWriteArraySet- Implementam leitura/escrita separada
- Mais eficientes em cenários com alta leitura e baixa escrita