1. Por que coleções comuns não servem para multithreading
Vamos relembrar como trabalhávamos com coleções no nosso aplicativo principal (por exemplo, uma sala de chat):
List<String> messages = new ArrayList<>();
messages.add("Olá!");
messages.add("Tudo bem?");
Em um programa de thread única, tudo funciona bem. Mas se várias threads ao mesmo tempo começarem a adicionar, remover ou ler elementos da mesma coleção — bem-vindo ao mundo das race conditions, estado inconsistente e bugs misteriosos.
Por exemplo, uma thread adiciona um elemento, outra remove, uma terceira itera — e, de repente, recebemos ConcurrentModificationException, às vezes até ArrayIndexOutOfBoundsException ou simplesmente uma coleção “corrompida”.
Clássico do gênero:
List<String> list = new ArrayList<>();
Runnable writer = () -> {
for (int i = 0; i < 1000; i++) {
list.add("msg-" + i);
}
};
Runnable reader = () -> {
for (String msg : list) {
// ...
}
};
// Execute writer e reader em threads diferentes — bugs garantidos!
Conclusão: Coleções comuns (ArrayList, HashMap, HashSet etc.) NÃO são thread-safe. Não se deve usá-las a partir de várias threads sem sincronização adicional (synchronized, locks etc.).
2. Quais coleções thread-safe existem em Java
A Java não deixa você na mão. Para tarefas multithread, no pacote java.util.concurrent há toda uma coleção de coleções (perdoe a tautologia) que podem ser usadas com segurança por várias threads.
Coleções thread-safe principais:
| Coleção | Onde usar | Características |
|---|---|---|
|
Map, cache, acesso frequente | Alto desempenho, sem lock global |
|
List, raramente muda, leitura frequente | Leituras rápidas, alterações lentas |
|
Set, raramente muda, leitura frequente | Semelhante ao List baseado em Copy-On-Write |
|
Fila, FIFO | Rápido, não bloqueante, filas de tarefas |
|
Map ordenado (NavigableMap) | Análogo thread-safe de TreeMap |
|
Set ordenado | Análogo thread-safe de TreeSet |
|
Filas com bloqueio (pools de threads) | Interface, várias implementações |
Importante! O bom e velho Collections.synchronizedList(list) e semelhantes — não são exatamente a mesma coisa que as coleções modernas de java.util.concurrent. Mais detalhes — logo abaixo.
3. ConcurrentHashMap: seu amigo no mundo do multithreading
ConcurrentHashMap<K, V> é, essencialmente, o mesmo HashMap, só que turbinado para multithreading. Ele permite que várias threads leiam e gravem dados simultaneamente com segurança, sem bloquear o mapa inteiro.
Em um HashMap comum, se você quiser tornar o acesso thread-safe, precisa colocar um lock na estrutura toda — e ela vira um “gargalo”: enquanto uma thread trabalha, as outras esperam.
ConcurrentHashMap resolve esse problema de forma mais inteligente. Nas versões antigas, o mapa era dividido em segmentos com locks separados; nas implementações novas, usam-se operações atômicas leves (CAS) no nível de buckets individuais. Graças a isso, as threads podem trabalhar em paralelo tranquilamente, desde que não mexam nos mesmos dados.
Exemplo de uso de ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap;
public class ChatStats {
private final ConcurrentHashMap<String, Integer> userMessageCount = new ConcurrentHashMap<>();
public void increment(String user) {
// Incrementa o valor de forma atômica
userMessageCount.merge(user, 1, Integer::sum);
}
public int getCount(String user) {
return userMessageCount.getOrDefault(user, 0);
}
}
O que importa aqui:
- Você pode chamar os métodos a partir de várias threads — tudo continuará correto.
- O método merge é atômico: se várias threads incrementarem o contador ao mesmo tempo, o resultado será correto.
- Leituras não precisam de sincronização adicional.
Por que ConcurrentHashMap é melhor que synchronizedMap?
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
Quando você usa synchronizedMap, qualquer operação — leitura, escrita ou remoção — bloqueia o mapa inteiro. Enquanto uma thread trabalha com os dados, as demais são obrigadas a esperar sua vez.
ConcurrentHashMap é bem mais elegante: permite que várias threads leiam e até modifiquem dados simultaneamente, desde que não acessem as mesmas regiões do mapa (buckets). Como resultado, em sistemas realmente multithread ele apresenta desempenho significativamente melhor — às vezes a diferença chega a dezenas de vezes.
4. CopyOnWriteArrayList e CopyOnWriteArraySet
CopyOnWriteArrayList e CopyOnWriteArraySet são coleções especiais que, a cada modificação (por exemplo, ao chamar add() ou remove()), criam uma nova cópia de todo o array. Em contrapartida, a leitura nelas ocorre sem qualquer sincronização e é totalmente segura para threads.
Imagine que você tem uma lista de convidados de uma festa. Cada vez que alguém chega ou vai embora, você reescreve a lista do zero e distribui cópias atualizadas para todos. Um pouco desperdiçador, mas ninguém se confundirá sobre quem está presente agora.
Quando isso é realmente conveniente
- Leituras acontecem com frequência, enquanto alterações são raras.
- Caso clássico — lista de listeners de eventos: handlers são adicionados esporadicamente, mas eventos chegam o tempo todo.
Exemplo: listeners do chat
import java.util.concurrent.CopyOnWriteArrayList;
public class ChatRoom {
private final CopyOnWriteArrayList<ChatListener> listeners = new CopyOnWriteArrayList<>();
public void addListener(ChatListener listener) {
listeners.add(listener);
}
public void removeListener(ChatListener listener) {
listeners.remove(listener);
}
public void sendMessage(String message) {
// Seguro para multithreading, mesmo que alguém esteja se inscrevendo/desinscrevendo neste exato momento
for (ChatListener listener : listeners) {
listener.onMessage(message);
}
}
}
Características importantes:
- Iterar sobre CopyOnWriteArrayList nunca lançará ConcurrentModificationException.
- Alterações (add/remove) são caras em tempo e memória (o array inteiro é copiado!).
- Não use para coleções grandes com alterações frequentes.
5. Outras coleções thread-safe
ConcurrentLinkedQueue
ConcurrentLinkedQueue é uma fila não bloqueante que funciona no esquema FIFO. Ela permite que várias threads adicionem e removam elementos com segurança ao mesmo tempo, sem o uso de locks explícitos. É muito usada para passar tarefas entre threads — rápida e sem “engarrafamentos”.
import java.util.concurrent.ConcurrentLinkedQueue;
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.add("task1");
String task = queue.poll(); // retorna null se a fila estiver vazia
ConcurrentSkipListMap e ConcurrentSkipListSet
- Análogos thread-safe de TreeMap e TreeSet.
- Os elementos estão sempre ordenados.
- Usados quando é importante manter a ordem das chaves.
import java.util.concurrent.ConcurrentSkipListMap;
ConcurrentSkipListMap<Integer, String> sortedMap = new ConcurrentSkipListMap<>();
sortedMap.put(10, "a");
sortedMap.put(2, "b");
System.out.println(sortedMap.firstEntry()); // 2=b
BlockingQueue e suas implementações
- Interface de fila que suporta operações de bloqueio (esperar até aparecer/liberar espaço).
- Implementações: ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue etc.
- Usada em pools de threads e no padrão “produtor-consumidor”.
import java.util.concurrent.ArrayBlockingQueue;
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10);
blockingQueue.put("task"); // Bloqueia se a fila estiver cheia
String t = blockingQueue.take(); // Bloqueia se a fila estiver vazia
6. Exemplos: operações seguras com coleções
Exemplo 1: Map thread-safe para contar mensagens
import java.util.concurrent.ConcurrentHashMap;
ConcurrentHashMap<String, Integer> messageCount = new ConcurrentHashMap<>();
// Thread 1
messageCount.put("Anna", 1);
// Thread 2
messageCount.put("Anna", messageCount.getOrDefault("Anna", 0) + 1); // Não é atômico!
// Correto (atômico):
messageCount.merge("Anna", 1, Integer::sum);
Exemplo 2: Iteração sobre CopyOnWriteArrayList
import java.util.concurrent.CopyOnWriteArrayList;
CopyOnWriteArrayList<String> users = new CopyOnWriteArrayList<>();
users.add("Anton");
users.add("Maria");
for (String user : users) {
System.out.println(user);
users.remove(user); // Não lançará ConcurrentModificationException!
}
System.out.println(users); // []
Exemplo 3: Fila de tarefas entre threads
import java.util.concurrent.ConcurrentLinkedQueue;
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
// Thread produtora
queue.add("task-1");
// Thread consumidora
String task = queue.poll(); // null se vazio
7. Nuances úteis
Quando (e por que) usar coleções thread-safe
Faz sentido usar coleções thread-safe quando:
- A mesma coleção é compartilhada entre várias threads.
- Você não quer sincronizar manualmente cada operação.
- É importante evitar race conditions e erros de consistência.
Cenários típicos:
- Cache em um sistema multithread (por exemplo, ConcurrentHashMap para armazenar sessões de usuários).
- Filas de tarefas entre threads (ConcurrentLinkedQueue, BlockingQueue).
- Listas de listeners de eventos (CopyOnWriteArrayList).
- Processamento de dados multithread (por exemplo, estilo MapReduce).
Limitações e armadilhas
- Operações sobre vários elementos não são atômicas. A construção if (!map.containsKey(k)) map.put(k, v) não é atômica. Use putIfAbsent, computeIfAbsent, merge.
- CopyOnWriteArrayList é ineficiente com alterações frequentes. Em tamanhos grandes e alterações frequentes (add/remove), a sobrecarga cresce rapidamente.
- A iteração sobre ConcurrentHashMap é “fraca”. A iteração fornece um snapshot fracamente consistente: você pode não ver parte das alterações paralelas.
- Coleções thread-safe não resolvem todos os problemas de sincronização. Se a lógica envolve várias coleções/variáveis ao mesmo tempo, será necessária sincronização externa (synchronized, locks, classes atômicas).
8. Erros comuns ao trabalhar com coleções thread-safe
Erro nº 1: Esperar mágica das coleções thread-safe. “Já que a coleção é thread-safe, posso fazer qualquer coisa e não pensar em sincronização”. Infelizmente, sequências de várias operações (verificação + adição) não são atômicas. Use métodos especializados: putIfAbsent, compute, merge.
Erro nº 2: Uso de CopyOnWriteArrayList para coleções grandes e frequentemente modificadas. Serve para listas de listeners, mas com 10.000+ elementos e alterações frequentes você terá grandes custos de memória e tempo.
Erro nº 3: ConcurrentModificationException ao usar coleções comuns. Você itera sobre ArrayList ou HashMap, enquanto outra thread altera a coleção — e você recebe ConcurrentModificationException. Use coleções especializadas ou bloqueie o acesso manualmente.
Erro nº 4: Esquecer a atomicidade de operações complexas. Se for necessário alterar várias coleções de uma vez ou executar uma série de ações relacionadas, as coleções thread-safe não vão ajudar. Aplique sincronização externa ou lógica transacional.
Erro nº 5: Erros ao iterar sobre ConcurrentHashMap. A iteração é fracamente consistente: não é possível usar o iterador como um “snapshot” consistente do estado do mapa. Para um snapshot consistente, copie os dados para uma estrutura separada.
GO TO FULL VERSION