7. Os threads de execução
7.1. Introdução
Quando se inicia um aplicativo, ele é executado em um fluxo de execução chamado thread. A classe que modela um thread é a classe java.lang.Thread, cujas propriedades e métodos são apresentados a seguir:
retorna o thread atualmente em execução | |
define o nome da thread | |
nome do thread | |
indica se o thread está ativo (true) ou não (false) | |
inicia a execução de um thread | |
método executado automaticamente após a execução do método start anterior | |
interrompe a execução de um thread por n milissegundos | |
operação bloqueante — aguarda o término da thread para avançar para a instrução seguinte |
Os construtores mais comumente utilizados são os seguintes:
cria uma referência a uma tarefa assíncrona. Esta ainda está inativa. A tarefa criada deve possuir o método run: na maioria das vezes, será utilizada uma classe derivada de Thread. | |
O mesmo, mas é o objeto Runnable, passado como parâmetro, que implementa o método run. |
Vejamos uma primeira aplicação que destaca a existência de um thread principal de execução, aquele no qual é executada a função main de uma classe:
// uso de threads
import java.io.*;
import java.util.*;
public class thread1{
public static void main(String[] arg)throws Exception {
// inicialização da thread atual
Thread main=Thread.currentThread();
// exibição
System.out.println("Thread courant : " + main.getName());
// alteração do nome
main.setName("myMainThread");
// verificação
System.out.println("Thread courant : " + main.getName());
// loop infinito
while(true){
// recuperando a hora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// exibição
System.out.println(main.getName() + " : " +H);
// parada temporária
Thread.sleep(1000);
}//while
}//main
}//classe
Resultados na tela:
Thread courant : main
Thread courant : myMainThread
myMainThread : 15:34:9
myMainThread : 15:34:10
myMainThread : 15:34:11
myMainThread : 15:34:12
Terminer le programme de commandes (O/N) ? o
O exemplo anterior ilustra os seguintes pontos:
- a função main é executada corretamente em um thread
- é possível acessar as características desse thread por meio de Thread.currentThread()
- a função do método sleep. Aqui, a thread que executa main entra em suspensão regularmente por 1 segundo entre duas exibições.
7.2. Criação de threads de execução
É possível ter aplicativos em que trechos de código são executados de forma “simultânea” em diferentes threads de execução. Quando se diz que os threads são executados simultaneamente, muitas vezes comete-se um equívoco de linguagem. Se a máquina tiver apenas um processador, como ainda é comum, os threads compartilham esse processador: cada um deles tem acesso a ele, por sua vez, por um breve instante (alguns milissegundos). É isso que dá a ilusão de paralelismo de execução. O tempo concedido a um thread depende de vários fatores, entre os quais sua prioridade, que tem um valor padrão, mas que também pode ser definida por programação. Quando um thread dispõe do processador, ele normalmente o utiliza durante todo o tempo que lhe foi concedido. No entanto, ele pode liberá-lo antes do tempo:
- entrando em espera por um evento (wait, join)
- entrando em modo de suspensão por um período determinado (sleep)
- Um thread T pode ser criado de diversas maneiras
- derivando da classe Thread e redefinindo o método run dessa classe.
- implementando a interface Runnable em uma classe e utilizando o construtor new Thread(Runnable). Runnable é uma interface que define apenas um único método: public void run(). O argumento do construtor anterior é, portanto, qualquer instância de classe que implemente esse método run.
No exemplo a seguir, os threads são criados por meio de uma classe anônima que deriva da classe Thread:
// criamos o thread i
tâches[i]=new Thread() {
public void run() {
affiche();
}
};//definição de tarefas[i]
O método run limita-se aqui a redirecionar para o método affiche.
- A execução da thread T é iniciada pelo método T.start(): esse método pertence à classe Thread e realiza algumas inicializações, em seguida, chama automaticamente o método run da thread ou da interface Runnable. O programa que executa a instrução T.start() não aguarda o término da tarefa T: ele passa imediatamente para a instrução seguinte. Assim, temos duas tarefas sendo executadas em paralelo. Frequentemente, elas precisam se comunicar entre si para saber em que ponto está o trabalho comum a ser realizado. Esse é o problema da sincronização de threads.
- Uma vez iniciado, o thread é executado de forma autônoma. Ele será interrompido quando a função run que ele está executando tiver concluído seu trabalho.
- É possível aguardar o término da execução do thread T por meio de T.join(). Trata-se de uma instrução bloqueante: o programa que a executa fica bloqueado até que a tarefa T tenha concluído seu trabalho. Essa também é uma forma de sincronização.
Vamos examinar o seguinte programa:
// uso de threads
import java.io.*;
import java.util.*;
public class thread2{
public static void main(String[] arg) {
// inicialização do thread atual
Thread main=Thread.currentThread();
// atribui-se um nome ao thread atual
main.setName("myMainThread");
// início da função main
System.out.println("début du thread " +main.getName());
// criação de threads de execução
Thread[] tâches=new Thread[5];
for(int i=0;i<tâches.length;i++){
// criação da thread i
tâches[i]=new Thread() {
public void run() {
affiche();
}
};//tarefas[i]
// define-se o nome da thread
tâches[i].setName(""+i);
// inicia-se a execução do thread i
tâches[i].start();
}//for
// fim da função main
System.out.println("fin du thread " +main.getName());
}//Main
public static void affiche() {
// recupera-se a hora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// exibição do início da execução
System.out.println("Début d'exécution de la méthode affiche dans le Thread " +
Thread.currentThread().getName()+ " : " + H);
// coloca em modo de espera por 1 s
try{
Thread.sleep(1000);
}catch (Exception ex){}
// recuperação da hora
calendrier=Calendar.getInstance();
H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// exibição do fim da execução
System.out.println("Fin d'exécution de la méthode affiche dans le Thread "
+Thread.currentThread().getName()+ " : " + H);
}// exibe
}//classe
O thread principal, aquele que executa a função main, cria outros 5 threads encarregados de executar o método estático affiche. Os resultados são os seguintes:
début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 15:48:3
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 1 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 2 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 3 : 15:48:3
Début d'exécution de la méthode affiche dans le Thread 4 : 15:48:3
Fin d'exécution de la méthode affiche dans le Thread 0 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 1 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 2 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 3 : 15:48:4
Fin d'exécution de la méthode affiche dans le Thread 4 : 15:48:4
Esses resultados são muito esclarecedores:
- vemos, em primeiro lugar, que o início da execução de um thread não é bloqueante. O método main iniciou a execução de 5 threads em paralelo e concluiu sua execução antes deles. A operação
inicia a execução da thread tarefas[i], mas, feito isso, a execução prossegue imediatamente com a instrução seguinte, sem aguardar o término da execução da thread.
- Todos os threads criados devem executar o método affiche. A ordem de execução é imprevisível. Mesmo que, no exemplo, a ordem de execução pareça seguir a ordem de inicialização dos threads, não se pode tirar conclusões gerais a partir disso. O sistema operacional possui, neste caso, 6 threads e um processador. Ele distribuirá o processador entre essas 6 threads de acordo com regras próprias.
- Nos resultados, observa-se uma consequência do método sleep. No exemplo, é a thread 0 que executa primeiro o método affiche. A mensagem de início de execução é exibida e, em seguida, ele executa o método sleep, que o suspende por 1 segundo. Ele então perde o processador, que fica disponível para outro thread. O exemplo mostra que é o thread 1 que o obterá. A thread 1 seguirá o mesmo caminho, assim como as outras threads. Quando o segundo de espera da thread 0 terminar, sua execução poderá ser retomada. O sistema lhe concede o processador e ela pode concluir a execução do método affiche.
Vamos modificar nosso programa para encerrar o método main com as instruções:
// fim da função
System.out.println("fin du thread " +main.getName());
// aplicativo encerrado
System.exit(0);
A execução do novo programa resulta em:
début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 1 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 2 : 16:5:45
Début d'exécution de la méthode affiche dans le Thread 3 : 16:5:45
fin du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 4 : 16:5:45
Assim que o método main executar a instrução:
ela interrompe todas as threads do aplicativo e não apenas a thread main. O método main pode querer aguardar o término da execução das threads que criou antes de encerrar a si mesmo. Isso pode ser feito com o método join da classe Thread:
// aguardando todos os threads
for(int i=0;i<tâches.length;i++){
// aguardando o thread i
tâches[i].join();
}//for
// fim da função
System.out.println("fin du thread " +main.getName());
// parada do aplicativo
System.exit(0);
Assim, obtêm-se os seguintes resultados:
début du thread myMainThread
Début d'exécution de la méthode affiche dans le Thread 0 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 1 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 2 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 3 : 16:11:9
Début d'exécution de la méthode affiche dans le Thread 4 : 16:11:9
Fin d'exécution de la méthode affiche dans le Thread 0 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 1 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 2 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 3 : 16:11:10
Fin d'exécution de la méthode affiche dans le Thread 4 : 16:11:10
fin du thread myMainThread
7.3. Importância dos threads
Agora que destacamos a existência de um thread padrão — aquele que executa o método Main — e que sabemos como criar outros, vamos nos deter na utilidade dos threads para nós e no motivo pelo qual os apresentamos aqui. Existe um tipo de aplicativo que se adapta bem ao uso de threads: os aplicativos cliente-servidor da Internet. Nesse tipo de aplicação, um servidor localizado em uma máquina S1 responde às solicitações de clientes localizados em máquinas remotas C1, C2, ..., Cn.

Todos os dias utilizamos aplicativos da Internet que se enquadram nesse esquema: serviços da Web, e-mail, consulta de fóruns, transferência de arquivos... No esquema acima, o servidor S1 deve atender os clientes Ci simultaneamente. Se tomarmos o exemplo de um servidor FTP (Protocolo de Transferência de Arquivos) que fornece arquivos aos seus clientes, sabemos que uma transferência de arquivo pode, às vezes, levar várias horas. É claro que está fora de questão que um único cliente monopolize o servidor por todo esse tempo. O que se faz normalmente é que o servidor crie tantos threads de execução quantos forem os clientes. Cada thread fica então encarregado de atender a um cliente específico. Como o processador é compartilhado ciclicamente entre todos os threads ativos da máquina, o servidor dedica um pouco de tempo a cada cliente, garantindo assim a simultaneidade do serviço.

7.4. Um relógio gráfico
Consideremos o seguinte aplicativo, que exibe uma janela com um relógio e um botão para parar ou reiniciar o relógio:
![]() | ![]() | ![]() |
Para que o relógio funcione, é necessário que um processo se encarregue de atualizar a hora a cada segundo. Ao mesmo tempo, é preciso monitorar os eventos que ocorrem na janela: quando o usuário clicar no botão “Parar”, será necessário interromper o relógio. Temos aqui duas tarefas paralelas e assíncronas: o usuário pode clicar a qualquer momento.
Consideremos o momento em que o relógio ainda não foi iniciado e o usuário clica no botão “Iniciar”. Trata-se de um evento clássico, e poderíamos pensar que um método da thread na qual a janela está sendo executada poderia, então, gerenciar o relógio. No entanto, quando um método do aplicativo gráfico está em execução, a thread desse aplicativo não está mais atenta aos eventos da interface gráfica. Esses eventos ocorrem e são colocados em uma fila para serem processados pelo aplicativo assim que o método atualmente em execução for concluído. No nosso exemplo do relógio, o método estará sempre em execução, já que somente o clique no botão “Parar” pode interrompê-lo. No entanto, esse evento só será processado quando o método for concluído. Estamos em um ciclo vicioso.
A solução para esse problema seria que, quando o usuário clicar no botão “Iniciar”, uma tarefa fosse iniciada para gerenciar o relógio, mas que o aplicativo pudesse continuar monitorando os eventos que ocorrem na janela. Teríamos, então, duas tarefas distintas sendo executadas em paralelo:
- gerenciamento do relógio
- monitoramento dos eventos da janela
Voltemos ao nosso relógio gráfico:
![]() |
|
O código útil do aplicativo desenvolvido com JBuilder é o seguinte:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
public class interfaceHorloge extends JFrame {
JPanel contentPane;
JTextField txtHorloge = new JTextField();
JButton btnGoStop = new JButton();
// atributos de instância
boolean finHorloge=true;
//Construir o quadro
public interfaceHorloge() {
enableEvents(AWTEvent.WINDOW_EVENT_MASK);
try {
jbInit();
}
catch(Exception e) {
e.printStackTrace();
}
}
private void runHorloge(){
// entramos em loop até que nos digam para parar
while( ! finHorloge){
// recuperamos a hora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// exibimos no campo T
txtHorloge.setText(H);
// espera de um segundo
try{
Thread.sleep(1000);
} catch (Exception e){
// saída com erro
System.exit(1);
}//tentar
}// enquanto
}// runHorloge
//Inicializar o componente
private void jbInit() throws Exception {
...................
}
//Substituído, para que possamos sair quando a janela for fechada
protected void processWindowEvent(WindowEvent e) {
.............
}
void btnGoStop_actionPerformed(ActionEvent e) {
// iniciamos/paramos o relógio
// recuperamos o texto do botão
String libellé=btnGoStop.getText();
// iniciar?
if(libellé.equals("Lancer")){
// criamos o thread no qual o relógio será executado
Thread thHorloge=new Thread(){
public void run(){
runHorloge();
}
};//encerrar thread
// autoriza-se o thread a iniciar
finHorloge=false;
// alteramos o texto do botão
btnGoStop.setText("Arrêter");
// inicia-se o thread
thHorloge.start();
// fim
return;
}//if
// parar
if(libellé.equals("Arrêter")){
// instrui a thread a parar
finHorloge=true;
// alteramos o texto do botão
btnGoStop.setText("Lancer");
// fim
return;
}//if
}
}
Quando o usuário clica no botão “Executar”, é criado um thread por meio de uma classe anônima:
O método run da thread remete ao método runHorloge do aplicativo. Feito isso, a thread é iniciada:
O método runHorloge será então executado:
private void runHorloge(){
// entramos em loop até que nos peçam para parar
while( ! finHorloge){
// recuperamos a hora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// exibimos no campo T
txtHorloge.setText(H);
// espera de um segundo
try{
Thread.sleep(1000);
} catch (Exception e){
// saída com erro
System.exit(1);
}//tentar
}// enquanto
}// runHorloge
O princípio do método é o seguinte:
- exibe a hora atual na caixa de texto txtHorloge
- pausa por 1 segundo
- repete a etapa 1, tendo o cuidado de testar previamente a variável booleana finHorloge, que será definida como verdadeira quando o usuário clicar no botão Arrêter.
7.5. Applet de relógio
Transformamos o aplicativo gráfico anterior em um applet pelo método habitual e criamos o documento HTML appletHorloge.htm a seguir:
<html>
<head>
<title>Applet Horloge</title>
</head>
<body>
<h2>Une applet horloge</h2>
<applet
code="appletHorloge.class"
width="150"
height="130"
></applet>
</center>
</body>
</html>
Quando carregamos diretamente este documento no IE clicando duas vezes nele, obtemos a seguinte exibição:

Todos os elementos necessários para o applet estão, neste exemplo, na mesma pasta:
E:\data\serge\Jbuilder\horloge\1>dir
13/06/2002 12:17 3 174 appletHorloge.class
13/06/2002 12:17 658 appletHorloge$1.class
13/06/2002 12:17 512 appletHorloge$2.class
13/06/2002 12:20 245 appletHorloge.htm
Nosso applet pode ser aprimorado. Mencionamos que, ao carregar o applet, o método init é executado e, em seguida, o método start, caso exista. Além disso, quando o usuário sai da página, o método stop é executado, caso exista. Quando ele retorna à página do applet, o método start é chamado novamente. Quando um applet utiliza threads de animação visual, costuma-se usar os métodos start e stop do applet para iniciar e parar as threads. De fato, não faz sentido que uma thread de animação visual continue trabalhando em segundo plano enquanto a animação está oculta.
Portanto, adicionamos ao nosso applet os seguintes métodos start e stop:
public void stop(){
// a página está oculta
// acompanhamento
System.out.println("Page stop");
// a página está oculta — encerramos o thread
finHorloge=true;
}
public void start(){
// a página reaparece
// acompanhamento
System.out.println("Page start");
// inicia-se um novo thread de temporizador, se necessário
if(btnGoStop.getText().equals("Arrêter")){
// altera-se o texto
btnGoStop.setText("Lancer");
// e simula-se que o usuário clicou nela
btnGoStop_actionPerformed(null);
}//if
}//iniciar
Além disso, adicionamos um monitoramento no método run da thread para saber quando ela inicia e termina:
private void runHorloge(){
// acompanhamento
System.out.println("Thread horloge lancé");
// entramos em um loop até que nos digam para parar
while( ! finHorloge){
// recuperamos a hora
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// exibimos no campo T
txtHorloge.setText(H);
// espera de um segundo
try{
Thread.sleep(1000);
} catch (Exception e){
// saída com erro
return;
}//try
}// enquanto
// acompanhamento
System.out.println("Thread horloge terminé");
}// runHorloge
Agora executamos o applet com AppletViewer:
E:\data\serge\Jbuilder\horloge\1>appletviewer appletHorloge.htm
Page start // applet iniciado - página exibida
Thread horloge lancé // o thread é iniciado em consequência
Page stop // applet minimizado
Thread horloge terminé // o thread é interrompido em consequência
Page start // applet exibida novamente
Thread horloge lancé // o thread é reiniciado
Thread horloge terminé // pressionado o botão “Parar”
Thread horloge lancé // botão “Iniciar” pressionado
Page stop // applet em ícone e
Thread horloge terminé // thread interrompido em consequência
Page start // reexibição do applet
Thread horloge lancé // thread reiniciado em consequência
Com o AppletViewer, o evento start ocorre quando a janela do AppletViewer está visível, e o evento stop ocorre quando ela é minimizada. Os resultados acima mostram que, quando o documento HTML está oculto, o thread é efetivamente interrompido, caso estivesse ativo.
7.6. Sincronização de tarefas
No nosso exemplo anterior, havia duas tarefas:
- a tarefa principal, representada pelo próprio aplicativo
- a tarefa responsável pelo relógio
A coordenação entre as duas tarefas era garantida pela tarefa principal, que definia um valor booleano para interromper o thread do relógio. Abordaremos agora o problema do acesso simultâneo de tarefas a recursos comuns, problema também conhecido como “compartilhamento de recursos”. Para ilustrá-lo, vamos primeiro estudar um exemplo.
7.6.1. Uma contagem não sincronizada
Consideremos a seguinte interface gráfica:

n.º | tipo | nome | função |
1 | JTextField | txtAGénérer | indica o número de threads a serem gerados |
2 | JTextfield (não editável) | txtGénéres | indica o número de threads gerados |
3 | JTextField (não editável) | txtStatus | fornece informações sobre os erros encontrados e sobre o próprio aplicativo |
4 | JButton | btnGénérer | inicia a geração de threads |
O funcionamento do aplicativo é o seguinte:
- o usuário indica o número de threads a serem gerados no campo 1
- ele inicia a geração desses threads com o botão 4
- os threads leem o valor do campo 2, incrementam-no e exibem o novo valor. Inicialmente, esse campo contém o valor 0.
Os threads gerados compartilham um recurso: o valor do campo 2. Nosso objetivo aqui é mostrar os problemas que surgem nessa situação. Veja um exemplo de execução:

Vemos que foi solicitada a geração de 1.000 threads e que apenas 7 foram contabilizadas. O código relevante do aplicativo é o seguinte:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class interfaceSynchro extends JFrame {
JPanel contentPane;
JLabel jLabel1 = new JLabel();
JTextField txtAGénérer = new JTextField();
JButton btnGénérer = new JButton();
JTextField txtStatus = new JTextField();
JTextField txtGénérés = new JTextField();
JLabel jLabel2 = new JLabel();
// variáveis de instância
Thread[] tâches=null; // os threads
int[] compteurs=null; // os contadores
//Construir o quadro
public interfaceSynchro() {
..........
}
//Inicializar o componente
private void jbInit() throws Exception {
......................
}
//Substituído, para que possamos sair quando a janela for fechada
protected void processWindowEvent(WindowEvent e) {
..................
}
void btnGénérer_actionPerformed(ActionEvent e) {
//geração de threads
// Lemos o número de threads a serem geradas
int nbThreads=0;
try{
// Lendo o campo que contém o número de threads
nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
// positivo >
if(nbThreads<=0) throw new Exception();
}catch(Exception ex){
//erro
txtStatus.setText("Nombre invalide");
// repetindo a operação
txtAGénérer.requestFocus();
return;
}//catch
// inicialmente, nenhum thread foi gerado
txtGénérés.setText("0"); // contador de tarefas em 0
// gerando e iniciando os threads
tâches=new Thread[nbThreads];
compteurs=new int[nbThreads];
for(int i=0;i<tâches.length;i++){
// criando o thread i
tâches[i]=new Thread() {
public void run() {
incrémente();
}
};//thread i
// define-se seu nome
tâches[i].setName(""+i);
// inicia-se sua execução
tâches[i].start();
}//for
}//gerar
// incrementar
private void incrémente(){
// recupera-se o número da thread
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// lê-se o valor do contador de tarefas
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// incrementa-se
compteurs[iThread]++;
// espera-se 100 milissegundos — o thread perderá então o processador
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// exibe-se o novo contador
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
// acompanhamento
System.out.println("Thread " + iThread + " : " + compteurs[iThread]);
}// incrementa
}// classe
Vamos detalhar o código:
- a janela declara duas variáveis de instância:
A matriz tâches será a matriz das threads geradas. A matriz compteurs será associada à matriz tâches. Cada tarefa terá um contador próprio para recuperar o valor do campo txtGénérés da interface gráfica.
- Ao clicar no botão Générer, o método btnGénérer_actionPerformed é executado.
- Este método começa recuperando o número de threads a serem geradas. Se necessário, é sinalizado um erro caso esse número não seja válido. Em seguida, ele gera as threads solicitadas, tomando o cuidado de registrar suas referências em um array e atribuindo um número a cada uma delas. O método run, que lida com os threads gerados, redireciona para o método incrémente da classe. Todos os threads são iniciados (start). A matriz de contadores associados aos threads também é criada.
- o método incrémente:
- lê o valor atual do campo txtGénérés e o armazena no contador pertencente à thread em execução
- pausa por 100 ms, a fim de liberar intencionalmente o processador
- exibe o novo valor no campo txtGénérés
Vamos explicar agora por que a contagem de threads está incorreta. Suponhamos que haja duas threads a serem geradas. Elas são executadas em uma ordem imprevisível. Uma delas passa primeiro e lê o valor 0 do contador. Em seguida, ele altera o valor para 1, mas não o grava na janela: ele se interrompe voluntariamente por 100 ms. Assim, perde o controle do processador, que é então cedido a outro thread. Este opera da mesma forma que o anterior: ele lê o contador da janela e recupera o 0 que ainda está lá. Ele altera o contador para 1 e, assim como o anterior, interrompe-se por 100 ms. O processador é então novamente atribuído ao primeiro thread: este vai gravar o valor 1 no contador da janela e encerrar sua execução. O processador é agora atribuído ao segundo thread, que também vai gravar 1. Temos um resultado incorreto.
De onde vem o problema? O segundo thread leu um valor incorreto porque o primeiro havia sido interrompido antes de concluir seu trabalho, que era atualizar o contador na janela. Isso nos leva ao conceito de recurso crítico e de seção crítica de um programa:
- um recurso crítico é um recurso que só pode ser mantido por um thread por vez. Aqui, o recurso crítico é o contador 2 da janela.
- uma seção crítica de um programa é uma sequência de instruções no fluxo de execução de um thread durante a qual ele acessa um recurso crítico. É preciso garantir que, durante essa seção crítica, ele seja o único a ter acesso ao recurso.
7.6.2. Contagem sincronizada por método
No exemplo anterior, cada thread executava o método incrémente da janela. O método incrémente estava declarado da seguinte forma:
Agora, vamos declará-lo de maneira diferente:
A palavra-chave synchronized significa que apenas um thread por vez pode executar o método incrémente. Consideremos as seguintes notações:
- o objeto janela F, que cria os threads em btnGénérer_actionPerformed
- duas threads T1 e T2 criadas por F
As duas threads são criadas por F e, em seguida, iniciadas. Portanto, ambas executarão o método F.run. Suponhamos que T1 chegue primeiro. Ele executa F.run e, em seguida, F.incremente, que é um método sincronizado. Ele lê o valor 0 do contador, o incrementa e, em seguida, fica parado por 100 ms. O processador é então cedido a T2, que, por sua vez, executa F.run e, em seguida, F.incremente. E aí ele fica bloqueado, pois a thread T1 está executando F.incremente, e a palavra-chave synchronized garante que apenas uma thread por vez possa executar F.incremente. T2, por sua vez, perde o processador sem ter conseguido ler o valor do contador. Após 100 ms, T1 recupera o processador, exibe o valor 1 do contador, sai de F.incremente e, em seguida, de F.run, e encerra sua execução. T2 recupera então o processador e, desta vez, pode executar F.incremente, pois T1 já não está mais executando esse método. T2 então lê o valor 1 do contador, o incrementa e fica parado por 100 ms. Após 100 ms, ele recupera o processador, exibe o valor 2 do contador e também é encerrado. Desta vez, o valor obtido está correto. Aqui está um exemplo testado:

7.6.3. Contagem sincronizada por um objeto
No exemplo anterior, o acesso ao contador txtGénérés foi sincronizado por um método. Se a janela que cria as threads for chamada de F, também podemos dizer que o método F.incremente representa um recurso que só deveria ser utilizado por uma única thread por vez. Trata-se, portanto, de um recurso crítico. O acesso sincronizado a esse recurso foi garantido pela palavra-chave synchronized:
Também se poderia dizer que o recurso crítico é o próprio objeto F. Isso é mais restritivo do que no caso em que o recurso crítico é F.incremente. De fato, neste último caso, se um thread T1 executar F.incremente, um thread T2 não poderá executar F.incremente, mas poderá executar outro método do objeto F, seja ele sincronizado ou não. No caso em que o próprio objeto F seja o recurso crítico, quando uma thread T1 executa uma seção sincronizada desse objeto, todas as outras seções sincronizadas do objeto ficam inacessíveis para as demais threads. Assim, se uma thread T1 executar o método sincronizado F.incremente, uma thread T2 não poderá executar não apenas F.incremente, mas também qualquer outra seção sincronizada de F, mesmo que nenhum thread a esteja utilizando. Portanto, trata-se de um método mais restritivo.
Suponhamos, então, que a janela se torne o recurso crítico. Escreveremos, então:
// incremento
private void incrémente(){
// seção crítica
synchronized(this){
// recupera-se o número da thread
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// lê-se o valor do contador de tarefas
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// incrementa-se
compteurs[iThread]++;
// espera-se 100 milissegundos — o thread perderá então o controle do processador
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// exibe-se o novo contador
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
}//synchronized
}// incrementa
Todas as threads utilizam a janela this para se sincronizarem. Na execução, obtêm-se os mesmos resultados corretos que anteriormente. Na verdade, é possível sincronizar-se com qualquer objeto conhecido por todas as threads. Aqui está, por exemplo, outro método que fornece os mesmos resultados:
// variáveis de instância
Thread[] tâches=null; // os threads
int[] compteurs=null; // os contadores
Object synchro=new Object(); // um objeto de sincronização de threads
// incrementa
private void incrémente(){
// seção crítica
synchronized(synchro){
..............
}//sincronizada
}// incremento
A janela cria um objeto do tipo Object que servirá para a sincronização das threads. Esse método é melhor do que aquele que se sincroniza com o objeto this, pois é menos restritivo. Nesse caso, se um thread T1 estiver na seção sincronizada de incrémente eum thread T2 quiser executar outra seção sincronizada do mesmo objeto this, mas sincronizada por um objeto diferente de synchro, ele poderá fazê-lo.
7.6.4. Sincronização por eventos
Desta vez, usamos um booleano peutPasser para indicar a um thread se ele pode ou não entrar em uma seção crítica. Uma escrita sem sincronização poderia ser a seguinte:
while(! peutPasser); // aguarda que peutPasser passe a verdadeiro
peutPasser=false; // nenhum outro thread deve passar
section critique; // aqui, a thread está sozinha
peutPasser=true; // outro thread pode passar pela seção crítica
A primeira instrução, na qual um thread fica em loop aguardando que peutPasser se torne verdadeiro, é ineficiente: o thread ocupa o processador desnecessariamente. Isso é chamado de espera ativa. É possível melhorar o código da seguinte forma:
while(! peutPasser){ // aguarda que peutPasser passe para verdadeiro
Thread.sleep(100); // parada por 100 ms
}
peutPasser=false; // nenhum outro thread deve passar
section critique; // aqui, a thread está sozinha
peutPasser=true; // outro thread pode passar pela seção crítica
O loop de espera aqui é melhor: se o thread não puder passar, ele entra em espera por 100 ms antes de verificar novamente se pode passar ou não. Enquanto isso, o processador será alocado a outros threads do sistema.
Esses dois métodos, na verdade, estão incorretos: eles não impedem que duas threads entrem simultaneamente na seção crítica. Suponhamos que uma thread T1 detecte que peutPasser está verdadeiro. Ele então passará para a instrução seguinte, onde redefinirá peutPasser como falso para bloquear as outras threads. No entanto, ele pode muito bem ser interrompido nesse momento, seja porque seu tempo de processador se esgotou, seja porque uma tarefa com prioridade maior solicitou o processador, ou por qualquer outro motivo. O resultado é que ele perde o processador. Ele o recuperará um pouco mais tarde. Enquanto isso, outras tarefas obterão o processador, entre as quais talvez um thread T2 que fica em loop aguardando que peutPasser passe para verdadeiro. Ele também descobrirá que peutPasser está em “true” (o primeiro thread não teve tempo de colocá-lo em “false”) e também entrará na seção crítica. O que não deveria ter acontecido.
A sequência
while(! peutPasser){ // aguarda que peutPasser passe para verdadeiro
try{
Thread.sleep(100); // pausa de 100 ms
} catch (Exception e) {}
}// enquanto
peutPasser=false; // nenhum outro thread deve passar
é uma sequência crítica que deve ser protegida por sincronização. Seguindo o exemplo anterior, pode-se escrever:
synchronized(synchro){
while(! peutPasser){ // aguardando que peutPasser passe para verdadeiro
try{
Thread.sleep(100); // pausa por 100 ms
} catch (Exception e) {}
}//enquanto
peutPasser=false; // nenhum outro thread deve passar
}// sincronizado
section critique; // aqui, a thread está sozinha
peutPasser=true; // outro thread pode passar pela seção crítica
Este exemplo funciona corretamente. É possível melhorá-lo evitando a espera semiativa da thread enquanto ela monitora regularmente o valor do booleano peutPasser. Em vez de acordar regularmente a cada 100 ms para verificar o estado de peutPasser, ele pode entrar em modo de espera e solicitar que seja acordado quando peutPasser estiver verdadeiro. Isso é escrito da seguinte maneira:
synchronized(synchro){
if (! peutPasser) {
try{
synchro.wait(); // se não for possível passar, então se espera
} catch (Exception e){
…
}
}
peutPasser=false; // nenhum outro thread deve passar
}// sincronizado
A operação synchro.wait() só pode ser executada por um thread que seja o “proprietário” momentâneo do objeto synchro. Aqui, a sequência é:
synchronized(synchro){
…
}// sincronizado
que garante que a thread seja proprietária do objeto synchro. Por meio da operação synchro.wait(), a thread cede a propriedade do bloqueio de sincronização. Por que isso acontece? Geralmente porque faltam recursos para que ela continue trabalhando. Assim, em vez de bloquear as outras threads que aguardam o recurso synchro, ele o cede e passa a aguardar o recurso que lhe falta. No nosso exemplo, ele aguarda que o booleano peutPasser passe para verdadeiro. Como ele será notificado desse evento? Da seguinte maneira:
synchronized(synchro){
if (! peutPasser) {
try{
synchro.wait(); // se não for possível passar, então aguardamos
} catch (Exception e){
…
}
}
peutPasser=false; // nenhum outro thread deve passar
}// sincronizado
section critique...
synchronized(synchro){
synchro.notify();
}
Vamos considerar o primeiro thread que passa pelo bloqueio de sincronização. Vamos chamá-lo de T1. Imaginemos que ele encontre o booleano peutPasser como verdadeiro, já que é o primeiro. Ele, portanto, o altera para falso. Em seguida, ele sai da seção crítica bloqueada pelo objeto synchro. Outra thread poderá então entrar na seção crítica para testar o valor de peutPasser. Ela o encontrará como falso e, então, entrará em espera por um evento (wait). Ao fazer isso, ela cede a propriedade do objeto synchro. Outro thread pode então entrar na seção crítica: ele também ficará em espera, pois peutPasser está em falso. Portanto, pode haver vários threads aguardando um evento no objeto synchro.
Voltemos à thread T1, que recebeu a vez. Ela executa a seção crítica e, em seguida, indica que outra thread já pode passar. Ela faz isso com a sequência:
synchronized(synchro){
synchro.notify();
}
Primeiro, ele precisa recuperar o objeto synchro com a instrução synchronized. Isso não deve representar nenhum problema, já que ele está competindo com threads que, caso obtenham momentaneamente o objeto synchro, terão que abandoná-lo por meio de um wait, pois consideram o peutPasser inválido. Portanto, nosso thread T1 acabará obtendo a propriedade do objeto synchro. Feito isso, ele indica, por meio da operação synchro.notify, que uma das threads bloqueadas por um synchro.wait deve ser despertada. Em seguida, ele abre mão novamente da propriedade do objeto synchro, que será então atribuída a uma das threads em espera. Este continua sua execução com a instrução que se segue ao wait que o havia colocado em espera. Por sua vez, ele executará a seção crítica e executará um synchro.notify para liberar outro thread. E assim por diante.
Vamos ver esse modo de funcionamento no exemplo da contagem já estudado.
void btnGénérer_actionPerformed(ActionEvent e) {
//geração de threads
// lê-se o número de threads a serem geradas
int nbThreads=0;
try{
// leitura do campo que contém o número de threads
nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
// positivo >
if(nbThreads<=0) throw new Exception();
}catch(Exception ex){
//erro
txtStatus.setText("Nombre invalide");
// repetindo a operação
txtAGénérer.requestFocus();
return;
}//catch
// RAZ contador de tarefas
txtGénérés.setText("0"); // contador de tarefas em 0
// o primeiro thread pode passar
peutPasser=true;
// geramos e iniciamos as threads
tâches=new Thread[nbThreads];
compteurs=new int[nbThreads];
for(int i=0;i<tâches.length;i++){
// cria-se o thread i
tâches[i]=new Thread() {
public void run() {
synchronise();
}
};//thread i
// define-se seu nome
tâches[i].setName(""+i);
// inicia-se sua execução
tâches[i].start();
}//for
}//gerar
Agora, os threads não executam mais o método incrémente, mas sim o método synchronise a seguir:
// etapa de sincronização das threads
public void synchronise(){
// solicita-se acesso à seção crítica
synchronized(synchro){
try{
// é possível passar?
if(! peutPasser){
System.out.println(Thread.currentThread().getName()+ " en attente");
synchro.wait();
}
// passamos — impedimos que outras threads passem
peutPasser=false;
} catch(Exception e){
txtStatus.setText(""+e);
return;
}//try
}// synchronized
// seção crítica
System.out.println(Thread.currentThread().getName()+ " passé");
incrémente();
// terminamos — liberamos qualquer thread que possa estar bloqueado na entrada da seção crítica
peutPasser=true;
System.out.println(Thread.currentThread().getName()+ " terminé");
synchronized(synchro){
synchro.notify();
}// sincronizado
} // sincroniza
O método synchronise tem como objetivo processar os threads um por um. Para isso, ele utiliza uma variável de sincronização synchro. O método incrémente não está mais protegido pela palavra-chave synchronized:
// incrementa
private void incrémente(){
// recuperamos o número do thread
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// lê-se o valor do contador de tarefas
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// incrementa-se
compteurs[iThread]++;
// aguarda 100 milissegundos — o thread perderá então o controle do processador
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// exibe o novo contador
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
}// incrementa
Para 5 threads, os resultados obtidos são os seguintes:
0 passé
1 en attente
2 en attente
3 en attente
4 en attente
0 terminé
1 passé
1 terminé
2 passé
2 terminé
3 passé
3 terminé
4 passé
4 terminé
Sejam T0 a T4 os 5 threads gerados pelo aplicativo. T0 é o primeiro a adquirir a propriedade do bloqueio synchro e verifica que peutPasser está verdadeiro. Ele define peutPasser como falso e segue em frente: esse é o significado da primeira mensagem passé. Muito provavelmente, ele continua e executa a seção crítica, especialmente o método incrémente. Nesse método, ele entra em espera por 100 ms (sleep). Assim, ele libera o processador. Este é alocado a outra thread, a thread T1, que passa a ter a propriedade do objeto synchro. Ele percebe que não consegue passar e entra em espera (wait). Em seguida, libera a propriedade do objeto synchro, bem como o processador. Este é alocado à thread T2, que sofre o mesmo destino. Durante os 100 ms de paralisação do T0, as threads T1 a T4 ficam, portanto, em espera. Esse é o significado das quatro mensagens “em espera”. Após 100 ms, o T0 recupera o processador e conclui seu trabalho: esse é o significado da mensagem “0 terminé”. Em seguida, ele libera uma das threads bloqueadas e é encerrado. O processador liberado é então alocado a uma thread disponível: aquela que acabou de ser liberada. Neste caso, é a T1. A thread T1 entra então na seção crítica: é esse o significado da mensagem “1 passé”. Ele faz o que precisa fazer e, por sua vez, fica parado por 100 ms. O processador fica então disponível para outro thread, mas todos estão aguardando um evento: nenhum deles pode assumir o processador. Após 100 ms, o thread T1 recupera o processador e é encerrado: esse é o significado da mensagem “1 terminé”. Os threads T1 a T4 terão o mesmo comportamento que o T1: esse é o significado das três séries de mensagens: “passado”, “concluído”.



