7. Os threads de execução
7.1. Introdução
Quando se inicia uma aplicação, esta é executada num fluxo de execução denominado «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 do 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 | |
suspende a execução de um thread durante n milissegundos | |
operação bloqueante — aguarda o fim do thread para passar à instrução seguinte |
Os construtores mais utilizados são os seguintes:
cria uma referência a uma tarefa assíncrona. Esta ainda se encontra 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 evidencia a existência de um thread principal de execução, aquele no qual é executada a função main de uma classe:
// utilização de threads
import java.io.*;
import java.util.*;
public class thread1{
public static void main(String[] arg)throws Exception {
// inicialização do thread atual
Thread main=Thread.currentThread();
// visualizaçã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){
// recuperar 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);
// paragem temporária
Thread.sleep(1000);
}//while
}//main
}//classe
Resultados no ecrã:
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 numa thread
- tem-se acesso às características desse thread através de Thread.currentThread()
- o papel do método sleep. Aqui, o thread que executa main entra em suspensão regularmente durante 1 segundo entre duas exibições.
7.2. Criação de threads de execução
É possível ter aplicações em que partes de código são executadas de forma «simultânea» em diferentes threads de execução. Quando se diz que os threads são executados simultaneamente, trata-se frequentemente de um uso incorreto da linguagem. Se a máquina tiver apenas um processador, como ainda é frequentemente o caso, os threads partilham esse processador: cada um dispõe dele, à vez, durante um breve instante (alguns milissegundos). É isso que dá a ilusão de paralelismo de execução. O tempo atribuído a um thread depende de vários fatores, incluindo a sua prioridade, que tem um valor por predefinição, mas que também pode ser definida por programação. Quando um thread dispõe do processador, utiliza-o normalmente durante todo o tempo que lhe foi atribuído. No entanto, pode libertá-lo antes do tempo:
- entrando em espera por um evento (wait, join)
- entrando em suspensão durante um período determinado (sleep)
- Um thread T pode ser criado de várias formas
- derivando da classe Thread e redefinindo o método run desta.
- implementando a interface Runnable numa 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 este método run.
No exemplo que se segue, os threads são criados utilizando uma classe anónima que deriva da classe Thread:
// cria-se 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 remeter para um método affiche.
- A execução do thread T é iniciada pelo método T.start(): este método pertence à classe Thread e realiza várias inicializações, lançando depois automaticamente o método run do Thread ou da interface Runnable. O programa que executa a instrução T.start() não aguarda a conclusão da tarefa T: passa imediatamente para a instrução seguinte. Temos, assim, duas tarefas a serem executadas em paralelo. Muitas vezes, estas têm de poder comunicar entre si para saber em que ponto se encontra o trabalho comum a realizar. Este é o problema da sincronização das threads.
- Uma vez iniciado, o thread executa-se de forma autónoma. Parará quando a função run que está a executar tiver concluído o seu trabalho.
- É possível aguardar o fim da execução do thread T através de T.join(). Trata-se aqui de uma instrução bloqueante: o programa que a executa fica bloqueado até que a tarefa T tenha concluído o seu trabalho. É também um método de sincronização.
Analisemos o seguinte programa:
// utilização 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 do 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 suspensão durante 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 mais 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
Estes resultados são muito esclarecedores:
- em primeiro lugar, verifica-se 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 terminou a sua execução antes deles. A operação
inicia a execução do thread «tâscas[i]», mas, feito isso, a execução prossegue imediatamente com a instrução seguinte, sem aguardar o fim da execução do thread.
- Todos os threads criados devem executar o método affiche. A ordem de execução é imprevisível. Embora, no exemplo, a ordem de execução pareça seguir a ordem de lançamento dos threads, não se podem tirar conclusões gerais a partir disso. O sistema operativo tem, neste caso, 6 threads e um processador. Irá distribuir o processador por estas 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 em primeiro lugar o método affiche. A mensagem de início de execução é apresentada e, em seguida, o método sleep é executado, o que o suspende durante 1 segundo. Perde então o processador, que fica assim disponível para outro thread. O exemplo mostra que é o thread 1 que o irá obter. A thread 1 seguirá o mesmo percurso, tal como as outras threads. Quando o segundo de suspensão da thread 0 terminar, a sua execução poderá retomar-se. O sistema atribui-lhe o processador e ela pode concluir a execução do método affiche.
Alteremos o nosso programa para concluir o método main com as instruções:
// fim da função
System.out.println("fin du thread " +main.getName());
// encerramento da aplicação
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 todos os threads da aplicação e não apenas o thread main. O método main poderá querer aguardar que as threads que criou terminem a sua execução antes de terminar ele próprio. Isto pode ser feito com o método join da classe Thread:
// aguarda todos os threads
for(int i=0;i<tâches.length;i++){
// aguarda-se o thread i
tâches[i].join();
}//for
// fim da função main
System.out.println("fin du thread " +main.getName());
// paragem da aplicação
System.exit(0);
Obtêm-se então 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 destacámos a existência de um thread por predefinição, aquele que executa o método Main, e que sabemos como criar outros, vamos debruçar-nos sobre a utilidade dos threads para nós e sobre a razão pela qual os apresentamos aqui. Existe um tipo de aplicações que se presta bem à utilização de threads: as aplicações cliente-servidor da Internet. Numa aplicação deste tipo, um servidor localizado numa máquina S1 responde aos pedidos de clientes localizados em máquinas remotas C1, C2, ..., Cn.

Utilizamos diariamente aplicações da Internet que se enquadram neste esquema: serviços Web, correio eletrónico, consulta de fóruns, transferência de ficheiros... No esquema acima, o servidor S1 deve servir os clientes Ci de forma simultânea. Se tomarmos o exemplo de um servidor FTP (File Transfer Protocol) que fornece ficheiros aos seus clientes, sabemos que uma transferência de ficheiros pode, por vezes, demorar várias horas. É claro que está fora de questão que um cliente monopolize sozinho o servidor durante tanto tempo. O que se faz habitualmente é o servidor criar 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 é partilhado ciclicamente entre todos os threads ativos da máquina, o servidor dedica algum tempo a cada cliente, garantindo assim a simultaneidade do serviço.

7.4. Um relógio gráfico
Consideremos a seguinte aplicação que apresenta 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 monitorizar os eventos que ocorrem na janela: quando o utilizador clicar no botão «Parar», será necessário parar o relógio. Temos aqui duas tarefas paralelas e assíncronas: o utilizador pode clicar a qualquer momento.
Consideremos o momento em que o relógio ainda não foi iniciado e o utilizador clica no botão «Iniciar». Trata-se de um evento clássico e poderíamos pensar que um método do thread no qual a janela está a ser executada poderia, então, gerir o relógio. No entanto, quando um método da aplicação gráfica está a ser executado, o thread dessa aplicação deixa de estar atento aos eventos da interface gráfica. Estes eventos ocorrem e são colocados numa fila para serem processados pela aplicação 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, uma vez que apenas o clique no botão «Parar» o pode interromper. No entanto, este evento só será processado quando o método estiver concluído. Estamos num ciclo vicioso.
A solução para este problema seria que, quando o utilizador clicar no botão «Iniciar», fosse lançada uma tarefa para gerir o relógio, mas que a aplicação pudesse continuar a ouvir os eventos que ocorrem na janela. Teríamos então duas tarefas distintas a serem executadas em paralelo:
- gestão do relógio
- escuta dos eventos da janela
Voltemos ao nosso relógio gráfico:
![]() |
|
O código útil da aplicação criada 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 da 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 num ciclo 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 a hora 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) {
// inicia/pára 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;
// alterar o texto do botão
btnGoStop.setText("Arrêter");
// inicia-se o thread
thHorloge.start();
// fim
return;
}//if
// parar
if(libellé.equals("Arrêter")){
// indica-se ao thread para parar
finHorloge=true;
// alteramos o texto do botão
btnGoStop.setText("Lancer");
// fim
return;
}//if
}
}
Quando o utilizador clica no botão «Iniciar», é criada uma thread utilizando uma classe anónima:
O método run da thread remete para o método runHorloge da aplicação. Feito isto, a thread é iniciada:
O método runHorloge será então executado:
private void runHorloge(){
// entramos num ciclo enquanto não nos for pedido 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);
// exibe-se no campo T
txtHorloge.setText(H);
// espera 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 durante 1 segundo
- repete o passo 1, tendo o cuidado de verificar previamente a variável booleana finHorloge, que será definida como verdadeira quando o utilizador clicar no botão Arrêter.
7.5. Applet do relógio
Transformamos a aplicação gráfica anterior num applet através do método habitual e criamos o seguinte documento HTML appletHorloge.htm:
<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 visualização:

Neste exemplo, todos os elementos necessários para o applet encontram-se 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
O nosso applet pode ser melhorado. Referimos que, ao carregar o applet, o método init era executado e, em seguida, o método start, caso existisse. Além disso, quando o utilizador sai da página, o método stop é executado, caso exista. Quando regressa à página do applet, o método start é novamente chamado. Quando um applet utiliza threads de animação visual, recorrem-se frequentemente aos métodos start e stop do applet para iniciar e parar as threads. Com efeito, não faz sentido que uma thread de animação visual continue a funcionar em segundo plano enquanto a animação está oculta.
Por isso, 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 - o thread é interrompido
finHorloge=true;
}
public void start(){
// a página reaparece
// acompanhamento
System.out.println("Page start");
// inicia-se um novo thread de relógio, se necessário
if(btnGoStop.getText().equals("Arrêter")){
// altera-se o texto
btnGoStop.setText("Lancer");
// e simula-se que o utilizador clicou nela
btnGoStop_actionPerformed(null);
}//if
}//iniciar
Além disso, adicionámos um acompanhamento no método run do thread para saber quando este inicia e termina:
private void runHorloge(){
// acompanhamento
System.out.println("Thread horloge lancé");
// entramos num ciclo 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
// seguimento
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 apresentada
Thread horloge lancé // o thread é iniciado em consequência
Page stop // applet minimizado
Thread horloge terminé // o thread é interrompido em conformidade
Page start // applet novamente exibida
Thread horloge lancé // o thread é reiniciado
Thread horloge terminé // clique no botão «Parar»
Thread horloge lancé // clique no botão «Iniciar»
Page stop // applet em ícone e
Thread horloge terminé // thread parado em consequência
Page start // reapresentação do applet
Thread horloge lancé // thread reiniciado em conformidade
Com o AppletViewer, o evento start ocorre quando a janela do AppletViewer está visível e o evento stop quando esta é 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 pela própria aplicação
- a tarefa responsável pelo relógio
A coordenação entre as duas tarefas era assegurada pela tarefa principal, que definia um valor booleano para parar o thread do relógio. Abordamos agora o problema do acesso simultâneo de tarefas a recursos comuns, problema também conhecido como «partilha de recursos». Para o ilustrar, 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 gerar |
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 a própria aplicação |
4 | JButton | btnGénérer | inicia a geração de threads |
O funcionamento da aplicação é o seguinte:
- o utilizador indica o número de threads a gerar no campo 1
- inicia a geração desses threads através do botão 4
- os threads lêem o valor do campo 2, incrementam-no e exibem o novo valor. Inicialmente, este campo contém o valor 0.
Os threads gerados partilham um recurso: o valor do campo 2. Pretendemos aqui demonstrar os problemas que surgem numa situação deste tipo. Eis um exemplo de execução:

Vemos que foi solicitada a geração de 1000 threads e que apenas 7 foram contabilizadas. O código relevante da aplicação é 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
// Lê-se o número de threads a gerar
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");
// recomeça
txtAGénérer.requestFocus();
return;
}//catch
// inicialmente, não foram gerados threads
txtGénérés.setText("0"); // contador de tarefas a 0
// estão a ser gerados e iniciados os 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() {
incrémente();
}
};//thread i
// define-se o seu nome
tâches[i].setName(""+i);
// inicia-se a sua execução
tâches[i].start();
}//for
}//gerar
// incrementar
private void incrémente(){
// recupera-se 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 vai então perder 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 analisar o código:
- a janela declara duas variáveis de instância:
// variáveis de instância
Thread[] tâches=null; // os threads
int[] compteurs=null; // os contadores
A matriz tâches será a matriz dos threads gerados. 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 por recuperar o número de threads a gerar. Se necessário, é sinalizado um erro caso esse número não seja válido. Em seguida, gera as threads solicitadas, tendo o cuidado de registar as suas referências numa matriz e atribuindo a cada uma delas um número. O método run dos threads gerados remete para o método incrémente da classe. Todos os threads são iniciados (start). É também criada a matriz de contadores associados aos threads.
- o método incrémente:
- lê o valor atual do campo txtGénérés e armazena-o no contador pertencente ao thread em execução
- pausa durante 100 ms, com o objetivo de libertar intencionalmente o processador
- exibe o novo valor no campo txtGénérés
Vamos agora explicar por que razão a contagem de threads está incorreta. Suponhamos que haja duas threads a gerar. Estas executam-se numa ordem imprevisível. Uma delas passa primeiro e lê o valor 0 do contador. Em seguida, altera-o para 1, mas não o escreve na janela: interrompe-se voluntariamente durante 100 ms. Perde então o processador, que é atribuído a outro thread. Este funciona da mesma forma que o anterior: lê o contador da janela e recupera o 0 que ainda lá se encontra. Altera o contador para 1 e, tal como o anterior, interrompe-se durante 100 ms. O processador é então novamente atribuído ao primeiro thread: este vai escrever o valor 1 no contador da janela e terminar. O processador é agora atribuído ao segundo thread, que também vai escrever 1. Obtém-se um resultado incorreto.
De onde vem o problema? O segundo thread leu um valor errado porque o primeiro tinha sido interrompido antes de terminar o seu trabalho, que consistia em atualizar o contador na janela. Isto leva-nos ao conceito de recurso crítico e de secção crítica de um programa:
- um recurso crítico é um recurso que só pode ser detido por um thread de cada vez. Aqui, o recurso crítico é o contador 2 da janela.
- uma secção crítica de um programa é uma sequência de instruções no fluxo de execução de um thread durante a qual este acede a um recurso crítico. É necessário garantir que, durante essa secção crítica, esse thread seja o único a ter acesso ao recurso.
7.6.2. Uma 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, declaramo-lo de forma diferente:
A palavra-chave synchronized significa que apenas um thread de cada 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
Ambas as threads são criadas por F e, em seguida, iniciadas. Assim, ambas irão executar o método F.run. Suponhamos que o T1 chegue primeiro. Este executa o F.run e, em seguida, o F.incremente, que é um método sincronizado. Lê o valor 0 do contador, incrementa-o e, em seguida, fica parado durante 100 ms. O processador é então atribuído a T2, que, por sua vez, executa F.run e, em seguida, F.incremente. E aí fica bloqueado, porque a thread T1 está a executar F.incremente e a palavra-chave synchronized garante que apenas uma thread de cada vez possa executar F.incremente. O T2 perde então, por sua vez, o acesso ao processador sem ter conseguido ler o valor do contador. Após 100 ms, o T1 recupera o processador, exibe o valor 1 do contador, sai do F.incremente e, em seguida, do F.run, e termina. O T2 recupera então o processador e, desta vez, consegue executar o F.incremente, uma vez que o T1 já não está a executar este método. O T2 lê então o valor 1 do contador, incrementa-o e pára durante 100 ms. Após 100 ms, recupera o processador, apresenta o valor 2 do contador e também termina. Desta vez, o valor obtido está correto. Eis 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 os threads se chamar F, também se pode dizer que o método F.incremente representa um recurso que só deveria ser utilizado por um único thread de cada vez. Trata-se, portanto, de um recurso crítico. O acesso sincronizado a este recurso foi garantido pela palavra-chave synchronized:
Também se poderia dizer que o recurso crítico é o próprio objeto F. Isto é mais restritivo do que no caso em que o recurso crítico é F.incremente. Com efeito, 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, quer este esteja sincronizado ou não. No caso de o objeto F ser ele próprio a recurso crítico, quando um thread T1 executa uma secção sincronizada desse objeto, todas as outras secções sincronizadas do objeto tornam-se inacessíveis para os outros threads. Assim, se uma thread T1 executar o método sincronizado F.incremente, uma thread T2 não poderá executar não só o F.incremente, mas também qualquer outra secção sincronizada de F, mesmo que nenhum thread a esteja a utilizar. Trata-se, portanto, de um método mais restritivo.
Suponhamos, então, que a janela se torne o recurso crítico. Escrever-se-á então:
// incremento
private void incrémente(){
// secção crítica
synchronized(this){
// recupera-se 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-se 100 milissegundos — o thread vai então perder 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]);
}//synchronized
}// incrementa
Todos os threads utilizam a janela this para se sincronizarem. Durante a execução, obtêm-se os mesmos resultados corretos que anteriormente. Na verdade, é possível sincronizar-se com qualquer objeto conhecido por todos os threads. Aqui está, por exemplo, outro método que dá 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
// incrementar
private void incrémente(){
// secção crítica
synchronized(synchro){
..............
}//sincronizada
}// incremento
A janela cria um objeto do tipo Object que servirá para a sincronização dos threads. Este método é melhor do que aquele que se sincroniza com o objeto this, porque é menos restritivo. Neste caso, se um thread T1 estiver na secção sincronizada de incrémente eum thread T2 pretender executar outra secção sincronizada do mesmo objeto «this», mas sincronizada por um objeto diferente de synchro, poderá fazê-lo.
7.6.4. Sincronização por eventos
Desta vez, utilizamos um valor booleano peutPasser para indicar a um thread se este pode ou não entrar numa secção crítica. Um código sem sincronização poderia ser o seguinte:
while(! peutPasser); // aguarda que peutPasser passe a verdadeiro
peutPasser=false; // nenhum outro thread deve passar
section critique; // aqui, o thread está sozinho
peutPasser=true; // outro thread pode passar pela secção crítica
A primeira instrução, em que um thread fica em loop à espera que peutPasser passe a verdadeiro, é pouco eficiente: o thread ocupa o processador desnecessariamente. Fala-se de espera ativa. É possível melhorar o código da seguinte forma:
while(! peutPasser){ // aguarda-se que peutPasser passe a verdadeiro
Thread.sleep(100); // paragem durante 100 ms
}
peutPasser=false; // nenhum outro thread deve passar
section critique; // aqui, o thread está sozinho
peutPasser=true; // outra thread pode passar pela secção crítica
O ciclo de espera é aqui melhor: se o thread não puder avançar, entra em suspensão durante 100 ms antes de verificar novamente se pode avançar ou não. Entretanto, o processador será atribuído a outros threads do sistema.
Na verdade, estes dois métodos estão incorretos: não impedem que duas threads entrem simultaneamente na secção crítica. Suponhamos que uma thread T1 deteta que peutPasser está a «verdadeiro». Passará então para a instrução seguinte, onde volta a definir peutPasser como falso para bloquear as outras threads. No entanto, pode muito bem ser interrompido nesse momento, seja porque o seu tempo de processador se esgotou, seja porque uma tarefa com maior prioridade solicitou o processador, ou por qualquer outra razão. O resultado é que perde o acesso ao processador. Recuperá-lo-á um pouco mais tarde. Entretanto, outras tarefas irão obter o acesso ao processador, entre as quais talvez um thread T2 que fica em loop à espera que peutPasser passe para verdadeiro. Este também irá descobrir que peutPasser está em «true» (o primeiro thread não teve tempo de o colocar em «false») e passará também para a secção crítica. O que não devia acontecer.
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. Inspirando-nos no exemplo anterior, podemos escrever:
synchronized(synchro){
while(! peutPasser){ // aguarda que peutPasser passe a verdadeiro
try{
Thread.sleep(100); // pausa durante 100 ms
} catch (Exception e) {}
}//enquanto
peutPasser=false; // nenhum outro thread deve passar
}// sincronizado
section critique; // aqui, o thread está sozinho
peutPasser=true; // outro thread pode passar pela secção crítica
Este exemplo funciona corretamente. É possível melhorá-lo evitando a espera semi-ativa da thread quando esta verifica regularmente o valor do booleano peutPasser. Em vez de acordar regularmente a cada 100 ms para verificar o estado de peutPasser, pode entrar em modo de suspensão e solicitar que seja acordado quando peutPasser estiver verdadeiro. Escreve-se isto da seguinte forma:
synchronized(synchro){
if (! peutPasser) {
try{
synchro.wait(); // se não for possível passar, então aguarda-se
} 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, nesse momento, o «proprietário» do objeto synchro. Neste caso, a sequência é a seguinte:
synchronized(synchro){
…
}// synchronized
que garante que o thread é o proprietário do objeto synchro. Através da operação synchro.wait(), o thread cede a propriedade do bloqueio de sincronização. Porquê? Geralmente porque lhe faltam recursos para continuar a trabalhar. Assim, em vez de bloquear os outros threads que aguardam a recurso synchro, cede-a e entra em espera pelo recurso de que necessita. No nosso exemplo, aguarda que o valor booleano peutPasser passe a verdadeiro. Como será notificado deste evento? Da seguinte forma:
synchronized(synchro){
if (! peutPasser) {
try{
synchro.wait(); // se não for possível avançar, então aguarda-se
} catch (Exception e){
…
}
}
peutPasser=false; // nenhum outro thread deve passar
}// sincronizado
section critique...
synchronized(synchro){
synchro.notify();
}
Consideremos o primeiro thread que passa pelo bloqueio de sincronização. Chamemos-lhe T1. Imaginemos que ele encontra o valor booleano peutPasser como verdadeiro, uma vez que é o primeiro. Assim, altera-o para falso. Em seguida, sai da secção crítica bloqueada pelo objeto synchro. Outra thread poderá então entrar na secção crítica para testar o valor de peutPasser. Encontrá-lo-á falso e ficará então em espera de um evento (wait). Ao fazê-lo, cede a propriedade do objeto synchro. Outro thread pode então entrar na secção crítica: também ficará em espera, pois peutPasser está em falso. Assim, podemos ter vários threads à espera de um evento no objeto synchro.
Voltemos à thread T1, à qual foi dada a vez. Esta executa a secção crítica e, em seguida, indica que outra thread pode agora passar. Faz-o com a sequência:
synchronized(synchro){
synchro.notify();
}
Primeiro, tem de recuperar a posse do objeto synchro com a instrução synchronized. Isso não deve representar qualquer problema, uma vez que está em competição com threads que, caso obtenham momentaneamente o objeto synchro, terão de o libertar através de um wait, porque consideram que o peutPasser está incorreto. Assim, o nosso thread T1 acabará por obter a posse do objeto synchro. Feito isto, indica, através da operação synchro.notify, que uma das threads bloqueadas por um synchro.wait deve ser reativada. Em seguida, abdica novamente da propriedade do objeto synchro, que será então atribuída a uma das threads em espera. Este prossegue a sua execução com a instrução que se segue ao wait que o tinha colocado em espera. Por sua vez, irá executar a secção crítica e executar um synchro.notify para libertar outro thread. E assim sucessivamente.
Vejamos este modo de funcionamento no exemplo da contagem já analisado.
void btnGénérer_actionPerformed(ActionEvent e) {
//geração de threads
// lê-se o número de threads a gerar
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");
// recomeça
txtAGénérer.requestFocus();
return;
}//catch
// RAZ contador de tarefas
txtGénérés.setText("0"); // contador de tarefas a 0
// O primeiro thread pode passar
peutPasser=true;
// geram-se e lançam-se os 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 o seu nome
tâches[i].setName(""+i);
// inicia-se a sua execução
tâches[i].start();
}//for
}//gerar
Agora, os threads já não executam o método incrémente, mas sim o método synchronise seguinte:
// fase de sincronização das threads
public void synchronise(){
// solicita-se o acesso à secção crítica
synchronized(synchro){
try{
// é possível avançar?
if(! peutPasser){
System.out.println(Thread.currentThread().getName()+ " en attente");
synchro.wait();
}
// já passámos — impedimos que os outros threads passem
peutPasser=false;
} catch(Exception e){
txtStatus.setText(""+e);
return;
}//try
}// synchronized
// secção crítica
System.out.println(Thread.currentThread().getName()+ " passé");
incrémente();
// terminámos — libertamos qualquer thread que possa estar bloqueado à entrada da secçã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 a um. Para tal, utiliza uma variável de sincronização synchro. O método incrémente já não está 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 vai então perder 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]);
}// 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 pela aplicação. O T0 é o primeiro a adquirir a propriedade do bloqueio synchro e verifica que o peutPasser está verdadeiro. Define peutPasser como falso e avança: é este o significado da primeira mensagem passé. Muito provavelmente, continua e executa a secção crítica, nomeadamente o método incrémente. Nesta, entra em espera durante 100 ms (sleep). Assim, liberta o processador. Este é atribuído a outro thread, o thread T1, que passa então a deter a propriedade do objeto synchro. Este descobre que não consegue passar e entra em espera (wait). Liberta então a propriedade do objeto synchro, bem como o processador. Este é atribuído à thread T2, que sofre o mesmo destino. Durante os 100 ms de paragem do T0, os threads T1 a T4 ficam, portanto, em espera. É este o significado das 4 mensagens «em espera». Após 100 ms, o T0 recupera o processador e conclui o seu trabalho: é esse o significado da mensagem «0 terminé». Em seguida, liberta uma das threads bloqueadas e termina. O processador libertado é então atribuído a uma thread disponível: aquela que acabou de ser libertada. Neste caso, é a T1. A thread T1 entra então na secção crítica: é esse o significado da mensagem «1 passé». Este executa o que tem de fazer e, por sua vez, fica inativo durante 100 ms. O processador fica então disponível para outro thread, mas todos estão à espera de um evento: nenhum deles pode ocupar o processador. Após 100 ms, o thread T1 recupera o processador e termina: é 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», «terminado».



