Concorrência, I/O e Swing

Coordenar threads, ler e escrever dados e responder a eventos numa interface Java.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Ler um ficheiro grande no listener de um botão impede a interface de responder. Uma thread de trabalho permite continuar a tratar eventos, mas o acesso de duas threads à mesma pontuação exige coordenação. Se ambas leem e escrevem o mesmo valor sem sincronização, uma atualização pode perder-se.

Começar e esperar por uma thread

Uma thread é um fluxo de execução dentro de um processo. Threads do mesmo processo podem partilhar objetos, mas têm pilhas de chamadas próprias. Concorrência significa que as tarefas podem avançar em períodos sobrepostos; não implica que corram em paralelo em todos os instantes.

Runnable descreve uma tarefa. new Thread(tarefa).start() cria uma execução concorrente e a JVM chama run. Chamar run() diretamente executa um método normal na thread atual. join() espera que a thread termine; não a inicia.

Este programa completo é para executar localmente com javac Main.java e java Main:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread trabalho = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("trabalho terminado");
            }
        });
        trabalho.start();
        trabalho.join();
        System.out.println("resultado recolhido");
    }
}

As duas linhas surgem nessa ordem porque join termina depois da thread. Sem join, não poderíamos garantir a ordem entre a impressão do trabalho e a do método main; um sleep não prova que outra tarefa acabou.

Uma operação composta pode perder alterações

contador++ contém leitura, soma e escrita. Se duas threads leem zero antes de qualquer uma escrever, ambas podem escrever um. Duas operações produziram apenas um incremento.

PassoThread AThread B
1Lê 0
2Lê 0
3Calcula 1 e escreve 1
4Calcula 1 e escreve 1

Uma race condition faz o resultado depender da intercalação. Uma data race ocorre quando acessos conflituosos ao mesmo estado, pelo menos um de escrita, não estão coordenados pela sincronização exigida. Não basta observar uma execução correta para concluir que não existe o problema.

Proteger estado com o mesmo lock

Um bloco synchronized adquire o monitor de um objeto. Só uma thread de cada vez entra em secções protegidas pelo mesmo monitor. Sair do monitor e voltar a adquiri-lo noutra thread também estabelece visibilidade das alterações segundo as regras de memória de Java.

Um método de instância synchronized usa this. Um método estático synchronized usa o objeto Class dessa classe. Dois objetos Contador diferentes não partilham automaticamente o mesmo lock.

Guarda este programa completo em Main.java e corre-o localmente:

class Contador {
    private int valor;

    synchronized void incrementar() { valor++; }
    synchronized int ler() { return valor; }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        final Contador contador = new Contador();
        Runnable tarefa = new Runnable() {
            public void run() {
                for (int i = 0; i < 1000; i++) contador.incrementar();
            }
        };
        Thread a = new Thread(tarefa);
        Thread b = new Thread(tarefa);
        a.start();
        b.start();
        a.join();
        b.join();
        System.out.println(contador.ler());
    }
}

A saída é 2000: cada incremento termina antes de outro entrar no mesmo contador. Todos os acessos concorrentes a esse estado têm de respeitar a mesma disciplina. volatile ajuda a publicar leituras e escritas de uma variável, mas não transforma valor++ numa operação atómica. Para um contador isolado, AtomicInteger.incrementAndGet() é outra opção.

Esperar por uma condição

Para esperar por uma condição sem largar a proteção do monitor de forma incoerente, wait() liberta o monitor e suspende a thread. Antes de retornar, volta a adquirir o monitor. notifyAll() acorda quem espera nesse objeto, mas a thread que notifica continua com o monitor até sair da secção.

A condição deve ser verificada num while. Quando a thread acorda, ainda precisa de readquirir o monitor antes de continuar, e outra thread pode chegar primeiro e mudar o estado outra vez. No exemplo dos dois consumidores, o segundo acorda para um valor que o primeiro já retirou. Há ainda despertares sem notificação, os chamados despertares espúrios, que a plataforma permite. O while volta a testar a condição com o monitor detido, nos dois casos. Chamamos estes métodos enquanto detemos o monitor correspondente.

Este excerto define uma caixa de um elemento; as chamadas seriam feitas por threads produtoras e consumidoras:

class Caixa {
    private Integer valor;

    synchronized void colocar(int novo) throws InterruptedException {
        while (valor != null) wait();
        valor = novo;
        notifyAll();
    }

    synchronized int retirar() throws InterruptedException {
        while (valor == null) wait();
        int resultado = valor;
        valor = null;
        notifyAll();
        return resultado;
    }
}

A caixa vazia tem valor == null. O produtor espera enquanto está cheia; o consumidor espera enquanto está vazia. Mudar o estado e notificar sob o mesmo monitor impede perder a coordenação entre o teste da condição e a espera. Numa aplicação, uma BlockingQueue costuma fornecer este comportamento sem implementares os monitores à mão.

Percorre uma execução com duas threads. O produtor coloca 1 e 2; o consumidor retira dois valores:

  1. Produtor adquire o monitor, vê valor == null, guarda 1, notifica e liberta.
  2. Consumidor adquire, vê valor == 1, retira, põe null, notifica e liberta.
  3. Produtor volta a adquirir, vê null, guarda 2, notifica.
  4. Consumidor adquire, retira 2.

Outra execução expõe por que a notificação não reserva o valor:

Ação sob o monitorCaixa depoisConsequência
C1 e C2 testam a caixa vazia e chamam wait()VaziaAmbos libertam o monitor e esperam.
Produtor guarda 1 e notifica todos1C1 e C2 podem tentar readquirir o monitor.
C1 readquire o monitor e retira 1VaziaC2 ainda não entrou.
C2 readquire o monitor e volta ao whileVaziaEspera novamente, sem ler null.

Com if, C2 continuaria depois de wait() sem testar de novo. Na caixa apresentada, tentaria converter o Integer nulo para int, lançando NullPointerException.

Se o consumidor chegar primeiro com a caixa vazia, entra no while, chama wait() e suspende, libertando o monitor; quando o produtor notifica, retoma, volta a adquirir o monitor e testa de novo a condição antes de ler.

Interrupção e deadlock

interrupt() pede a uma thread que interrompa o trabalho. Não a termina à força. O código consulta o sinal ou reage a InterruptedException em operações como sleep, wait e join. Se capturas a exceção e não a propagas, normalmente restaura o sinal com Thread.currentThread().interrupt() e termina a tarefa de forma coerente.

Um deadlock aparece, por exemplo, quando A detém o lock 1 e espera pelo 2, enquanto B detém o 2 e espera pelo 1. Adquirir vários locks sempre pela mesma ordem evita este ciclo nesse cenário. Mantém as secções curtas e evita operações externas ou bloqueantes enquanto deténs locks.

Bytes, texto e buffers

InputStream e OutputStream transportam bytes. Reader e Writer transportam texto. Um InputStreamReader descodifica bytes segundo uma codificação; não assumes que a codificação predefinida é igual em todas as máquinas.

BufferedReader acrescenta um buffer e permite ler linhas. Streams binárias como DataInputStream leem tipos primitivos segundo um formato próprio; esse formato não é o texto decimal de um inteiro.

Ler texto UTF-8 a partir de bytes
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

public class Main {
    public static void main(String[] args) throws IOException {
        byte[] dados = "Ana:3\nRita:5\n".getBytes(StandardCharsets.UTF_8);
        int total = 0;
        try (BufferedReader leitor = new BufferedReader(
        new InputStreamReader(new ByteArrayInputStream(dados),
        StandardCharsets.UTF_8))) {
            String linha;
            while ((linha = leitor.readLine()) != null) {
                String[] partes = linha.split(":");
                total += Integer.parseInt(partes[1]);
            }
        }
        System.out.println(total);
    }
}
Resultado

Executa o código para ver a saída e os gráficos.

Obtemos 8. A origem é memória, para poderes executar o exemplo no navegador. readLine() devolve null no fim, não uma string vazia. O exemplo pressupõe linhas válidas no formato nome:inteiro; um importador real deve tratar linhas incompletas e números inválidos.

O try-with-resources fecha o recurso também se ocorrer uma exceção. Fechar o BufferedReader fecha o leitor e a stream que envolve. A ordem de construção segue os dados, bytes, descodificação, buffer e leitura por linha.

Ficheiros e recursos da aplicação

Este programa completo escreve e lê um ficheiro local. Guarda-o em Main.java e corre javac Main.java seguido de java Main na pasta onde queres criar pontos.txt.

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class Main {
    public static void main(String[] args) throws IOException {
        Path caminho = Paths.get("pontos.txt");
        try (BufferedWriter escritor = Files.newBufferedWriter(
                caminho, StandardCharsets.UTF_8)) {
            escritor.write("120");
            escritor.newLine();
        }
        try (BufferedReader leitor = Files.newBufferedReader(
                caminho, StandardCharsets.UTF_8)) {
            System.out.println(leitor.readLine());
        }
    }
}

A saída é 120. O programa cria ou substitui pontos.txt na pasta atual. Um recurso empacotado em src/main/resources tem outro ciclo de vida: faz parte da aplicação e pode estar dentro de um JAR. Usa getResourceAsStream para o ler, em vez de assumir que existe como ficheiro normal no disco. Verifica a possibilidade de a stream ser null quando o recurso não existe.

Swing e eventos

Em Swing, JFrame é uma janela, JPanel agrupa componentes e JButton emite eventos. Um layout manager coloca os componentes de acordo com regras. Evita coordenadas absolutas para a organização da interface, porque tamanhos de letra e janelas variam.

A Event Dispatch Thread, EDT, processa eventos e atualiza a interface. Cria e altera a interface nessa thread. Um listener que demora bloqueia os outros eventos e o redesenho. Trabalho longo corre num SwingWorker, que separa três momentos. Segue o percurso EDT para worker para EDT:

  1. O listener, na EDT, cria o worker, chama execute() e termina logo, libertando a interface.
  2. doInBackground() corre numa thread de trabalho: lê o ficheiro e devolve uma string. Nunca toca em componentes Swing aqui.
  3. done() corre de novo na EDT: chama get() para obter o resultado, trata exceções da leitura e atualiza o JLabel.

O excerto seguinte mostra só o listener; somar, valor e lerFicheiroGrande() pertencem ao programa da interface.

somar.addActionListener(evento -> {
    SwingWorker<String, Void> tarefa = new SwingWorker<String, Void>() {
        @Override
        protected String doInBackground() throws Exception {
            return lerFicheiroGrande();
        }
        @Override
        protected void done() {
            try {
                valor.setText(get());
            } catch (Exception ex) {
                valor.setText("falha a ler");
            }
        }
    };
    tarefa.execute();
});

Chamar get() no listener enquanto a tarefa decorre voltaria a bloquear a EDT. Envolver tudo em invokeLater sem worker só adia o mesmo bloqueio.

Este programa completo requer um ambiente gráfico local, não o editor do navegador:

import java.awt.BorderLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class Main {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                JFrame janela = new JFrame("Pontuacao");
                final int[] pontos = {0};
                final JLabel valor = new JLabel("0");
                JButton somar = new JButton("Somar ponto");
                somar.addActionListener(evento -> {
                    pontos[0]++;
                    valor.setText(Integer.toString(pontos[0]));
                });
                JPanel painel = new JPanel(new BorderLayout(12, 12));
                painel.add(valor, BorderLayout.CENTER);
                painel.add(somar, BorderLayout.SOUTH);
                janela.setContentPane(painel);
                janela.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                janela.pack();
                janela.setLocationRelativeTo(null);
                janela.setVisible(true);
            }
        });
    }
}

Cada clique acrescenta um ponto. O array de um elemento serve apenas para guardar o estado deste exemplo curto; no MVC, colocaremos o estado num modelo próprio. O listener e a criação da janela correm na EDT, por isso este estado não é partilhado com outra thread.

Para desenho personalizado, redefine paintComponent(Graphics g) e chama super.paintComponent(g). Usa o Graphics recebido, desenha a partir do estado e pede uma atualização com repaint(). Não uses uma chamada direta a getGraphics() como forma de manter um desenho: o próximo redesenho pode apagá-lo.

Exercícios

Dois locks para o mesmo contador

Duas threads incrementam o mesmo campo. Uma sincroniza no objeto A e outra no objeto B, com A e B distintos. O incremento está protegido da concorrência entre elas?

A tua resposta

Pista
Identifica o objeto usado como monitor em cada bloco.
Mais uma pista
A thread que detém A não impede outra de adquirir B.
Solução

Não. A disciplina tem de proteger todos os acessos ao campo com o mesmo monitor, ou usar uma operação atómica adequada. Dois synchronized em objetos diferentes não criam essa exclusão.

  • Sim, qualquer synchronized impede todas as threads de entrar.. A exclusão só se aplica ao mesmo monitor.
  • Não, cada monitor permite que uma thread entre ao mesmo tempo.. As threads não disputam o mesmo lock.
  • Sim, desde que ambas usem volatile.. Volatile não torna a operação composta ++ atómica.

Erros frequentes

Não confundas visibilidade com indivisibilidade.
Voltar à explicação
Ler e fechar uma stream de texto

Recebes bytes UTF-8 de um ficheiro. Indica como ler linhas e garantir o fecho também quando uma linha tem um número inválido.

Pista
A transformação bytes para texto precisa de uma codificação.
Mais uma pista

Um BufferedReader pode envolver o InputStreamReader e ser o recurso do try.

Solução

Constrói InputStreamReader(stream, StandardCharsets.UTF_8), envolve num BufferedReader e coloca-o num try-with-resources. readLine devolve null no fim. Um NumberFormatException é um erro do conteúdo e o recurso fecha na mesma ao sair do try.

Confere a tua resposta:

  • Usa InputStreamReader com UTF-8.
  • Envolve num BufferedReader para ler linhas.
  • Usa try-with-resources e distingue erro de formato de fim da stream.

Erros frequentes

Uma linha vazia não é o fim da stream. Não ignores uma exceção e continues com dados incompletos.

Voltar à explicação
Acordar não reserva o valor

Dois consumidores esperam numa caixa vazia. O produtor coloca um único valor e chama notifyAll(). O primeiro consumidor retoma e retira-o; só depois o segundo readquire o monitor. Explica por que while (valor == null) wait() está correto e trocar while por if está errado neste cenário.

Pista

Inspeciona o estado quando o segundo consumidor entra, não quando foi notificado.

Mais uma pista

Com if, o segundo consumidor continua depois de wait sem voltar a testar valor.

Solução

Percorre o cenário: produtor coloca um valor e notifica; o primeiro consumidor retoma primeiro, adquire o monitor e retira. Com while, o segundo consumidor vê novamente valor == null e volta a esperar. Com if, passa diretamente à leitura, embora o primeiro já tenha retirado o valor. Na caixa da lição, converter esse Integer nulo para int lança NullPointerException. A notificação permite tentar retomar; não entrega um valor a cada thread nem substitui o teste da condição.

Confere a tua resposta:

  • Reconhece que o segundo consumidor readquire o monitor com a caixa vazia.
  • Volta a verificar a condição e espera por outro valor.
  • Distingue notificação de reserva do recurso.

Erros frequentes

synchronized impede acesso simultâneo, mas não garante que a condição continua verdadeira quando outra thread readquire o monitor.

Voltar à explicação
Um listener que lê um ficheiro grande

Um listener de um botão lê um ficheiro grande durante cinco segundos e só depois atualiza um JLabel. Durante a leitura, os botões e o redesenho deixam de responder. Explica o bloqueio e distribui a leitura e a atualização entre SwingWorker.doInBackground() e done().

Pista
A mesma thread trata listeners e redesenho.
Mais uma pista

Executar uma tarefa em segundo plano e atualizar a interface têm requisitos diferentes.

Solução

O listener ocupa a EDT durante cinco segundos, por isso os eventos seguintes esperam. O listener inicia o SwingWorker com execute() e termina. doInBackground() lê os dados numa thread de trabalho e devolve um resultado. done() corre na EDT, obtém o resultado e atualiza o JLabel. Trata a falha da tarefa para apresentar um erro em vez de um resultado inventado. Chamar get() no listener enquanto a tarefa decorre voltaria a bloquear a EDT.

Confere a tua resposta:

  • Identifica que o listener ocupa a EDT.
  • Executa a leitura em doInBackground sem alterar componentes Swing.
  • Atualiza o JLabel em done na EDT e trata uma falha da leitura.

Erros frequentes

invokeLater com toda a leitura só adia o bloqueio para a EDT. Uma thread de trabalho também não deve alterar o JLabel diretamente.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.