Concorrência Multithreading em Java

A utilização de pools de threads evita a criação e destruição frequentes de threads, permitindo a reutilização desses objetos. Além disso, os pools permitem um controle flexível do número de concorrências no projeto.

Para diagnosticar problemas como loops infinitos, deadlocks, bloqueios ou lentidão na abertura de páginas, é útil obter um dump de threads. O dump de threads, ou pilha de threads, pode ser obtido em dois passos:

  1. Identifique o PID da thread usando o comando jps. Em sistemas Linux, também pode-se usar ps -ef | grep java.
  2. Imprima a pilha de threads usando o comando jstack pid. Em sistemas Linux, o comando kill -3 pid também pode ser utilizado.

O método getStackTrace() da classe Thread também pode ser usado para obter a pilha de uma thread específica.

Para verificar se uma thread detém um monitor de objeto, utilize o método estático holdsLock(Object obj) da classe Thread. Este método retornará true apenas se o monitor do objeto estiver sendo detido por alguma thread.

A diferença entre synchronized e ReentrantLock reside em que synchronized é uma palavra-chave, enquanto ReentrantLock é uma classe. ReentrantLock oferece mais flexibilidade, podendo ser herdada, ter métodos próprios e variáveis de classe. Entre suas vantagens estão:

  • Possibilidade de definir um tempo limite para a aquisição de um lock, evitando assim deadlocks.
  • Acesso a informações sobre locks.
  • Múltiplos mecanismos de notificação.

O ConcurrentHashMap tem um grau de concorrência, que é o tamanho dos segmentos, com valor padrão de 16. Isso significa que até 16 threads podem operar simultaneamente no ConcurrentHashMap, tornando-o superior ao Hashtable.

ReentrantLock pode ser limitado em alguns cenários, como quando múltiplas threads leem dados sem necessidade de bloqueio. Para esses casos, utiliza-se ReadWriteLock, onde leituras são compartilhadas e escritas são exclusivas.

FutureTask representa uma tarefa de computação assíncrona. Aceita um objeto Callable, permitindo aguardar o resultado, verificar se a tarefa foi concluída ou cancelá-la. Como FutureTask implementa Runnable, pode ser executado em um pool de threads.

Para identificar qual thread está utilizando mais CPU em um ambiente Linux, siga estes passos:

  1. Obtenha o PID do projeto com jps ou ps -ef | grep java.
  2. Execute top -H -p pid.

A saída mostrará a porcentagem de uso da CPU por cada thread. LWP representa o ID da thread no sistema operacional. Para localizar a pilha de uma thread com alto uso de CPU, combine "top -H -p pid" com "jps pid".

Caso uma thread lancee uma exceção não capturada, ela será interrompida e qualquer monitor de objeto que ela detivesse será liberado imediatamente.

Dados podem ser compartilhados entre threads através de objetos comuns e métodos como wait/notify/notifyAll ou await/signal/signalAll. Classes como BlockingQueue foram projetadas especificamente para esse propósito.

Um processo contém pelo menos uma thread, e a velocidade de execução do programa pode ser aumentada com múltiplas threads.

Daemon threads são processos em segundo plano que realizam tarefas periódicas ou respondem a eventos. No Java, o coletor de lixo é um exemplo de daemon thread.

Há três maneiras de criar threads: herdar a classe Thread e sobrescrever o método run, implementar a interface Runnable, ou implementar a interface Callable.

A principal diferença entre Runnable e Callable é que Callable retorna um valor e pode lançar exceções, enquanto Runnable não.

Os estados de uma thread em Java incluem NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING e TERMINATED.

sleep() e wait() têm diferenças importantes: sleep() vem da classe Thread e não libera o lock, enquanto wait() vem da classe Object e libera o lock. sleep() acorda automaticamente após um tempo determinado, enquanto wait() pode ser acordado com notify() ou notifyAll().

notifyAll() acorda todas as threads aguardando, enquanto notify() acorda apenas uma delas. notifyAll() move todas as threads da fila de espera para a fila de bloqueio, permitindo a competição pelo lock. notify(), por outro lado, acorda apenas uma thread, cuja escolha é arbitrária.

start() inicia uma nova thread e invoca o método run(). run() pode ser chamado várias vezes, mas start() apenas uma vez.

Há sete maneiras de criar pools de threads, sendo a mais fundamental a última:

  • newSingleThreadExecutor(): um único thread, com uma fila de trabalho ilimitada.
  • newCachedThreadPool(): cria threads conforme necessário, reutilizando threads ociosas.
  • newFixedThreadPool(int nThreads): usa um número fixo de threads.
  • newSingleThreadScheduledExecutor(): um único thread para execução programada.
  • newScheduledThreadPool(int corePoolSize): múltiplos threads para execução programada.
  • newWorkStealingPool(int parallelism): usa algoritmo Work-Stealing para paralelismo.
  • ThreadPoolExecutor(): criação manual de pools de threads.

Os estados de um pool de threads são RUNNING, SHUTDOWN, STOP, TIDYING e TERMINATED.

execute() aceita apenas tarefas Runnable, enquanto submit() aceita tanto Runnable quanto Callable.

Para garantir a segurança em multithreading em Java, use classes seguras, como aquelas em java.util.concurrent, ou sincronize o acesso aos recursos compartilhados.

O sincronizado em Java utiliza monitores e tem diferentes níveis de otimização, incluindo locks biasados, leves e pesados.

Um deadlock ocorre quando duas ou mais threads estão esperando por recursos que a outra já detém.

Para evitar deadlocks, use timeouts, classes de concorrência do Java, reduza o escopo das seções sincronizadas e minimize a quantidade de código sincronizado.

ThreadLocal permite que cada thread tenha uma cópia independente de uma variável, útil em cenários como gerenciamento de conexões de banco de dados e sessões.

Synchroinzed é implementado por instruções monitorenter e monitorexit, com otimizações como locks biasados, leves e pesados introduzidas no Java 6.

Volatile garante visibilidade de alterações em variáveis, mas não atomicidade. Synchronized garante ambos.

Synchronized pode ser aplicado a classes, métodos e blocos de código, enquanto Lock só pode ser usado em blocos.

ReentrantLock oferece maior flexibilidade, mas requer cuidado para garantir o release do lock, evitando deadlocks.

Atomic classes utilizam CAS (Compare And Swap), volatile e métodos nativos para operações atômicas, evitando overhead de synchronized.

A construção de uma thread, blocos estáticos e o método run são chamados pela thread que instancia a thread, não pela própria thread.

Sincronizar apenas partes do código (blocos) é geralmente preferível a sincronizar todo o método.

A escolha da configuração do pool de threads depende do tipo de carga: alta concorrência com tarefas curtas, baixa concorrência com tarefas longas, ou ambas.

Tipos de Locks em Java

Java suporta diversos tipos de locks, incluindo:

  • Fair Locks vs Non-Fair Locks: Fair Locks garantem a ordem de chegada, enquanto Non-Fair Locks não.
  • Reentrant Locks vs Non-Reentrant Locks: Reentrant Locks permitem que uma thread adquira o mesmo lock múltiplas vezes.
  • Exclusive Locks vs Shared Locks: Exclusive Locks são detidos por uma única thread, enquanto Shared Locks podem ser compartilhados.
  • Mutual Exclusion Locks vs Read-Write Locks: Mutual Exclusion Locks permitem acesso exclusivo a um recurso, enquanto Read-Write Locks permitem múltiplos leitores ou um único escritor.
  • Optimistic Locks vs Pessimistic Locks: Optimistic Locks assumem que conflitos raramente ocorrem, enquanto Pessimistic Locks assumem o contrário.
  • Segmented Locks: Usado em estruturas como ConcurrentHashMap para melhorar a concorrência.
  • Biased Locks, Light Weight Locks e Heavy Weight Locks: Níveis de otimização do sincronizado.
  • Spin Locks: Threads ocupam a CPU em loop até adquirirem o lock.

Exemplo de Spin Lock em Java:

public class SpinLock {
    private AtomicReference<Thread> owner = new AtomicReference<>();
    public void lock() {
        Thread current = Thread.currentThread();
        while (!owner.compareAndSet(null, current)) {
            // Espera ocupada
        }
    }
    public void unlock() {
        Thread current = Thread.currentThread();
        owner.compareAndSet(current, null);
    }
}

Exemplo de Reentrant Spin Lock em Java:

public class ReentrantSpinLock {
    private AtomicReference<Thread> owner = new AtomicReference<>();
    private int count;
    public void lock() {
        Thread current = Thread.currentThread();
        if (current == owner.get()) {
            count++;
            return;
        }
        while (!owner.compareAndSet(null, current)) {
            // Espera ocupada
        }
    }
    public void unlock() {
        Thread cur = Thread.currentThread();
        if (cur == owner.get()) {
            if (count > 0) {
                count--;
            } else {
                owner.compareAndSet(cur, null);
            }
        }
    }
}

Tags: java Concurrency ThreadPools synchronized ReentrantLock

Publicado em 10-9 14:43