CodeGym /Cursos /JAVA 25 SELF /Coleções thread-safe: ConcurrentHashMap e outras

Coleções thread-safe: ConcurrentHashMap e outras

JAVA 25 SELF
Nível 53 , Lição 2
Disponível

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
ConcurrentHashMap
Map, cache, acesso frequente Alto desempenho, sem lock global
CopyOnWriteArrayList
List, raramente muda, leitura frequente Leituras rápidas, alterações lentas
CopyOnWriteArraySet
Set, raramente muda, leitura frequente Semelhante ao List baseado em Copy-On-Write
ConcurrentLinkedQueue
Fila, FIFO Rápido, não bloqueante, filas de tarefas
ConcurrentSkipListMap
Map ordenado (NavigableMap) Análogo thread-safe de TreeMap
ConcurrentSkipListSet
Set ordenado Análogo thread-safe de TreeSet
BlockingQueue
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.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION