Conceitos de JUC - Parte 4: Coleções Thread-Safe em Java

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 AbstractList e implementa List, 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 volatile para 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 synchronized para 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:
  • CopyOnWriteArrayList e CopyOnWriteArraySet
  • Implementam leitura/escrita separada
  • Mais eficientes em cenários com alta leitura e baixa escrita

Tags: java Concurrency juc thread-safety Collections

Publicado em 7-11 03:19