Programação Multithread em Java

1.1 Introdução ao Multithreading

É uma técnica que permite a execução concorrente de múltiplos threads, seja em software ou hardware. Sistemas com suporte a multithreading podem executar vários threads simultaneamente, melhorando o desempenho geral.

1.2 Concorrência vs Paralelismo

  • Paralelismo: Múltiplas instruções sendo executadas simultaneamente em múltiplos CPUs.
  • Concorrência: Múltiplas instruções sendo executadas alternadamente em um único CPU.

1.3 Processos e Threads

  • Processo: Um programa em execução
    • Independência: Cada processo é uma unidade independente de execução e alocação de recursos.
    • Dinamicismo: Processos têm ciclo de vida (criação, execução, término).
    • Concorrência: Vários processos podem executar simultaneamente.
  • Thread: Um fluxo de controle único dentro de um processo
    • Thread única: Processo com um único fluxo de execução.
    • Múltiplas threads: Processo com múltiplos fluxos de execução.

1.4 Implementação de Multithreading: Herança da Classe Thread

Métodos Principais

Método Descrição
void run() Chamado após o thread iniciar, contém o código a ser executado
void start() Inicia a execução do thread, invoca o método run() pela JVM

Passos de Implementação

  1. Definir uma classe que herda de Thread
  2. Sobrescrever o método run()
  3. Criar instância da classe
  4. Iniciar o thread

Exemplo de Código

public class MinhaThread extends Thread {
    @Override
    public void run() {
        for(int i=0; i<100; i++) {
            System.out.println("Thread " + Thread.currentThread().getName() + ": " + i);
        }
    }
}

public class ExemploThread {
    public static void main(String[] args) {
        MinhaThread t1 = new MinhaThread();
        MinhaThread t2 = new MinhaThread();
        
        t1.start();
        t2.start();
    }
}

1.5 Implementação de Multithreading: Interface Runnable

Construtores da Classe Thread

Construtor Descrição
Thread(Runnable target) Cria um novo objeto Thread
Thread(Runnable target, String name) Cria um novo objeto Thread com nome específico

Passos de Implementação

  1. Definir uma classe que implementa Runnable
  2. Sobrescrever o método run()
  3. Criar instância da classe
  4. Criar objeto Thread passando a instância
  5. Iniciar o thread

Exemplo de Código

public class MinhaTarefa implements Runnable {
    @Override
    public void run() {
        for(int i=0; i<100; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}

public class ExemploRunnable {
    public static void main(String[] args) {
        MinhaTarefa tarefa = new MinhaTarefa();
        
        Thread t1 = new Thread(tarefa, "Processador 1");
        Thread t2 = new Thread(tarefa, "Processador 2");
        
        t1.start();
        t2.start();
    }
}

1.6 Implementação de Multithreading: Interface Callable

Métodos Principais

Método Descrição
V call() Executa a tarefa e retorna um resultado
FutureTask(Callable callable) Cria um FutureTask que executa o Callable
V get() Espera o término e obtém o resultado

Passos de Implementação

  1. Definir uma classe que implementa Callable
  2. Sobrescrever o método call()
  3. Criar instância da classe
  4. Criar objeto FutureTask
  5. Criar objeto Thread com o FutureTask
  6. Iniciar o thread
  7. Obter o resultado com get()

Exemplo de Código

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class MinhaTarefaComResultado implements Callable<string> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println("Processando item " + i);
        }
        return "Tarefa concluída com sucesso";
    }
}

public class ExemploCallable {
    public static void main(String[] args) {
        MinhaTarefaComResultado tarefa = new MinhaTarefaComResultado();
        
        FutureTask<string> futuro = new FutureTask<>(tarefa);
        Thread t1 = new Thread(futuro);
        
        t1.start();
        
        try {
            String resultado = futuro.get();
            System.out.println("Resultado: " + resultado);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}</string></string>

1.7 Comparação dos Métodos de Implementação

Abordagem Vantagens Desvantagens
Runnable/Callable Maior flexibilidade de herança Mais complexo, não usa diretamente métodos de Thread
Herança de Thread Simples de implementar Restringe herança única

1.8 Nome de Threads

Métodos Principais

Método Descrição
void setName(String name) Define o nome do thread
String getName() Retorna o nome do thread
Thread currentThread() Retorna a referência do thread atual

Exemplo de Código

public class ThreadComNome extends Thread {
    public ThreadComNome(String nome) {
        super(nome);
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName() + ": " + i);
        }
    }
}

public class ExemploNomeThread {
    public static void main(String[] args) {
        ThreadComNome t1 = new ThreadComNome("Servidor A");
        ThreadComNome t2 = new ThreadComNome("Servidor B");
        
        t1.start();
        t2.start();
        
        System.out.println("Thread principal: " + Thread.currentThread().getName());
    }
}

1.9 Espera de Threads (Sleep)

Método Principal

Método Descrição
static void sleep(long millis) Pausa a execução do thread por milissegundos

Exemplo de Código

public class TarefaComEspera implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(1000); // Espera 1 segundo
                System.out.println(Thread.currentThread().getName() + ": Contagem " + i);
            } catch (InterruptedException e) {
                System.out.println("Thread interrompida");
            }
        }
    }
}

public class ExemploSleep {
    public static void main(String[] args) {
        TarefaComEspera tarefa = new TarefaComEspera();
        
        Thread t1 = new Thread(tarefa, "Timer 1");
        Thread t2 = new Thread(tarefa, "Timer 2");
        
        t1.start();
        t2.start();
    }
}

1.10 Prioridade de Threads

Agendamento de Threads

  • Modelo de tempo compartilhado: Todos os threads usam o CPU igualmente
  • Modelo de preempção: Threads com prioridade maior usam mais o CPU

Java usa modelo de preempção, mas a execução é devido à natureza aleatória do agendamento.

Métodos de Prioridade

Método Descrição
int getPriority() Retorna a prioridade do thread
void setPriority(int newPriority) Define a prioridade (1-10, padrão 5)

Exemplo de Código

public class TarefaPrioridade implements Runnable {
    @Override
    public void run() {
        System.out.println("Executando " + Thread.currentThread().getName() 
            + " com prioridade " + Thread.currentThread().getPriority());
    }
}

public class ExemploPrioridade {
    public static void main(String[] args) {
        TarefaPrioridade tarefa = new TarefaPrioridade();
        
        Thread t1 = new Thread(tarefa, "Alta Prioridade");
        t1.setPriority(Thread.MAX_PRIORITY); // 10
        
        Thread t2 = new Thread(tarefa, "Baixa Prioridade");
        t2.setPriority(Thread.MIN_PRIORITY); // 1
        
        t1.start();
        t2.start();
    }
}

1.11 Threads de Demon (Daemon)

Método Principal

Método Descrição
void setDaemon(boolean on) Marca o thread como de demon

Exemplo de Código

public class ThreadPrincipal extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + ": " + i);
        }
    }
}

public class ThreadDemonio extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println(getName() + ": Em segundo plano");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ExemploDemonio {
    public static void main(String[] args) {
        ThreadPrincipal principal = new ThreadPrincipal();
        ThreadDemonio demonio = new ThreadDemonio();
        
        demonio.setDaemon(true); // Define como thread de demon
        
        principal.start();
        demonio.start();
        
        try {
            principal.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Thread principal terminada");
    }
}
  1. Sincronização de Threads

2.1 Problema de Venda de Ingressos

Requisito

Cinema com 100 ingressos e 3 guichês para venda. Simular o processo de venda.

Implementação

public class VendaIngressos implements Runnable {
    private int ingressos = 100;
    
    @Override
    public void run() {
        while (true) {
            if (ingressos <= 0) {
                break;
            } else {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ingressos--;
                System.out.println(Thread.currentThread().getName() + " vendeu um ingresso. Restam: " + ingressos);
            }
        }
    }
}

public class ExemploVenda {
    public static void main(String[] args) {
        VendaIngressos venda = new VendaIngressos();
        
        Thread t1 = new Thread(venda, "Guichê 1");
        Thread t2 = new Thread(venda, "Guichê 2");
        Thread t3 = new Thread(venda, "Guichê 3");
        
        t1.start();
        t2.start();
        t3.start();
    }
}

2.2 Problemas no Exemplo

  • Mesmo ingresso sendo vendido múltiplas vezes
  • Números negativos de ingressos

Causa: Aleatoriedade na execução dos threads levando a condições de corrida.

2.3 Bloco Sincronizado

Condições para Problemas

  • Ambiente multithread
  • Dados compartilhados
  • Múltiplas instruções operando os dados

Solução

Usar blocos sincronizados para garantir que apenas um thread execute a crítica seção por vez:

Exemplo de Código

public class VendaIngressosSegura implements Runnable {
    private int ingressos = 100;
    private final Object lock = new Object();
    
    @Override
    public void run() {
        while (true) {
            synchronized (lock) {
                if (ingressos <= 0) {
                    break;
                } else {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ingressos--;
                    System.out.println(Thread.currentThread().getName() + " vendeu um ingresso. Restam: " + ingressos);
                }
            }
        }
    }
}

2.4 Métodos Sincronizados

Sintaxe

modificador synchronized tipoRetorno nomeMétodo(parâmetros) {
    // Código sincronizado
}

Objeto de Lock

  • Método de instância: this
  • Método estático: NomeDaClasse.class

Exemplo de Código

public class GerenciadorIngressos {
    private static int ingressos = 100;
    
    public static synchronized boolean venderIngresso() {
        if (ingressos <= 0) {
            return false;
        }
        
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        ingressos--;
        System.out.println(Thread.currentThread().getName() + " vendeu um ingresso. Restam: " + ingressos);
        return true;
    }
}

public class Vendedor implements Runnable {
    @Override
    public void run() {
        while (GerenciadorIngressos.venderIngresso()) {
            // Continuar vendendo
        }
    }
}

2.5 Lock Interface

Vantagens

Mais controle sobre o lock em comparação com synchronized.

Métodos Principais

Método Descrição
void lock() Adquire o lock
void unlock() Liberar o lock

Exemplo de Código

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SistemaReserva {
    private int assentos = 100;
    private final Lock lock = new ReentrantLock();
    
    public boolean reservarAssento() {
        lock.lock();
        try {
            if (assentos <= 0) {
                return false;
            }
            
            Thread.sleep(100);
            assentos--;
            System.out.println(Thread.currentThread().getName() + " reservou um assento. Restam: " + assentos);
            return true;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return false;
        } finally {
            lock.unlock();
        }
    }
}

public class Cliente implements Runnable {
    private SistemaReserva sistema;
    
    public Cliente(SistemaReserva sistema) {
        this.sistema = sistema;
    }
    
    @Override
    public void run() {
        while (sistema.reservarAssento()) {
            // Tentar reservar novamente
        }
    }
}

2.6 Deadlock

Conceito

Situação onde dois ou mais threads ficam bloqueados permanentemente, cada um aguardando o outro liberar um recurso.

Condições

  • Recursos exclusivos
  • Sincronização aninhada

Exemplo de Código

public class ExemploDeadlock {
    private static final Object RECURSO_A = new Object();
    private static final Object RECURSO_B = new Object();
    
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (RECURSO_A) {
                System.out.println("Thread 1: Adquiriu Recurso A");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (RECURSO_B) {
                    System.out.println("Thread 1: Adquiriu Recurso B");
                }
            }
        });
        
        Thread t2 = new Thread(() -> {
            synchronized (RECURSO_B) {
                System.out.println("Thread 2: Adquiriu Recurso B");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (RECURSO_A) {
                    System.out.println("Thread 2: Adquiriu Recurso A");
                }
            }
        });
        
        t1.start();
        t2.start();
    }
}
  1. Padrão Produtor-Consumidor

3.1 Visão Geral

Padrão clássico de colaboração entre threads onde:

  • Produtor: Cria dados e os coloca em uma área compartilhada
  • Consumidor: Consome dados da área compartilhada

Métodos de Espera e Notificação

Método Descrição
void wait() Faz o thread esperar até ser notificado
void notify() Acorda um thread em espera
void notifyAll() Acorda todos os threads em espera

3.2 Exemplo Básico

Código

public class BufferCompartilhado {
    private int dado = -1;
    private boolean temDado = false;
    
    public synchronized void put(int valor) {
        while (temDado) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        
        this.dado = valor;
        this.temDado = true;
        notifyAll();
    }
    
    public synchronized int get() {
        while (!temDado) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        
        int valor = this.dado;
        this.temDado = false;
        notifyAll();
        return valor;
    }
}

public class Produtor implements Runnable {
    private BufferCompartilhado buffer;
    
    public Produtor(BufferCompartilhado buffer) {
        this.buffer = buffer;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            buffer.put(i);
            System.out.println("Produziu: " + i);
            try {
                Thread.sleep((int)(Math.random() * 100));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

public class Consumidor implements Runnable {
    private BufferCompartilhado buffer;
    
    public Consumidor(BufferCompartilhado buffer) {
        this.buffer = buffer;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            int valor = buffer.get();
            System.out.println("Consumiu: " + valor);
            try {
                Thread.sleep((int)(Math.random() * 100));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

public class ExemploProdutorConsumidor {
    public static void main(String[] args) {
        BufferCompartilhado buffer = new BufferCompartilhado();
        
        Thread produtorThread = new Thread(new Produtor(buffer), "Produtor");
        Thread consumidorThread = new Thread(new Consumidor(buffer), "Consumidor");
        
        produtorThread.start();
        consumidorThread.start();
    }
}

3.3 Filas de Bloqueio (BlockingQueue)

Vantagens

Implementação pronta do padrão produtor-consumidor com sincronização embutida.

Tipos Comuns

  • ArrayBlockingQueue: Baseada em array, com capacidade fixa
  • LinkedBlockingQueue: Baseada em lista, capacidade teoricamente ilimitada

Métodos Principais

Método Descrição
put(E e) Insere elemento, bloqueia se cheio
take() Remove elemento, bloqueia se vazio

Exemplo de Código

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class SistemaFila {
    private BlockingQueue<string> fila;
    
    public SistemaFila(int capacidade) {
        this.fila = new ArrayBlockingQueue<>(capacidade);
    }
    
    public void adicionarTarefa(String tarefa) throws InterruptedException {
        fila.put(tarefa);
        System.out.println("Tarefa adicionada: " + tarefa);
    }
    
    public String processarTarefa() throws InterruptedException {
        String tarefa = fila.take();
        System.out.println("Processando: " + tarefa);
        return tarefa;
    }
}

public class Trabalhador implements Runnable {
    private SistemaFila sistema;
    
    public Trabalhador(SistemaFila sistema) {
        this.sistema = sistema;
    }
    
    @Override
    public void run() {
        try {
            while (true) {
                String tarefa = sistema.processarTarefa();
                // Simular processamento
                Thread.sleep((long)(Math.random() * 1000));
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

public class GerenciadorTarefas implements Runnable {
    private SistemaFila sistema;
    
    public GerenciadorTarefas(SistemaFila sistema) {
        this.sistema = sistema;
    }
    
    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                sistema.adicionarTarefa("Tarefa-" + i);
                Thread.sleep((long)(Math.random() * 500));
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

public class ExemploBlockingQueue {
    public static void main(String[] args) {
        SistemaFila sistema = new SistemaFila(5);
        
        Thread gerenciador = new Thread(new GerenciadorTarefas(sistema), "Gerenciador");
        Thread trabalhador1 = new Thread(new Trabalhador(sistema), "Trabalhador 1");
        Thread trabalhador2 = new Thread(new Trabalhador(sistema), "Trabalhador 2");
        
        gerenciador.start();
        trabalhador1.start();
        trabalhador2.start();
    }
}</string>

Tags: java Multithreading Concorrência Sincronização threads

Publicado em 7-6 05:17