# Concorrência, I/O e Swing

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

Página: https://resumos.rgo.pt/cadeiras/ldts/java-concorrencia-io-swing/

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.

| Passo | Thread A | Thread B |
| --- | --- | --- |
| 1 | Lê 0 |  |
| 2 |  | Lê 0 |
| 3 | Calcula 1 e escreve 1 |  |
| 4 |  | Calcula 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 monitor | Caixa depois | Consequência |
| --- | --- | --- |
| C1 e C2 testam a caixa vazia e chamam `wait()` | Vazia | Ambos libertam o monitor e esperam. |
| Produtor guarda 1 e notifica todos | 1 | C1 e C2 podem tentar readquirir o monitor. |
| C1 readquire o monitor e retira 1 | Vazia | C2 ainda não entrou. |
| C2 readquire o monitor e volta ao `while` | Vazia | Espera 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.

**Main.java**

```java
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);
    }
}
```

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](https://resumos.rgo.pt/cadeiras/ldts/mvc-projeto/), 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?

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](https://resumos.rgo.pt/cadeiras/ldts/java-concorrencia-io-swing/#proteger-estado-com-o-mesmo-lock)

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](https://resumos.rgo.pt/cadeiras/ldts/java-concorrencia-io-swing/#bytes-texto-e-buffers)

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](https://resumos.rgo.pt/cadeiras/ldts/java-concorrencia-io-swing/#esperar-por-uma-condi%C3%A7%C3%A3o)

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](https://resumos.rgo.pt/cadeiras/ldts/java-concorrencia-io-swing/#swing-e-eventos)
