7. Threads
7.1. Introdução
Quando uma aplicação é iniciada, ela é executada num fluxo de execução denominado thread. A classe que modela um thread é a classe java.lang.Thread, que possui as seguintes propriedades e métodos:
retorna a thread atualmente em execução | |
define o nome da thread | |
nome da thread | |
indica se a thread está ativa (true) ou não (false) | |
inicia a execução de uma thread | |
método executado automaticamente após a execução do método start anterior | |
pausa a execução de um thread por n milissegundos | |
operação de bloqueio — aguarda que a thread termine antes de prosseguir para a instrução seguinte |
Os construtores mais utilizados são os seguintes:
cria uma referência a uma tarefa assíncrona. Esta tarefa ainda está inativa. A tarefa criada deve ter um método run: na maioria das vezes, será utilizada uma classe derivada de Thread. | |
Igual ao anterior, mas o objeto Runnable passado como parâmetro implementa o método run. |
Vejamos uma aplicação simples que demonstra a existência de um thread de execução principal — aquele em que a função main de uma classe é executada:
// use of threads
import java.io.*;
import java.util.*;
public class thread1{
public static void main(String[] arg)throws Exception {
// init current thread
Thread main=Thread.currentThread();
// display
System.out.println("Thread courant : " + main.getName());
// we change the name
main.setName("myMainThread");
// check
System.out.println("Thread courant : " + main.getName());
// infinite loop
while(true){
// time recovery
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// display
System.out.println(main.getName() + " : " +H);
// temporary shutdown
Thread.sleep(1000);
}//while
}//hand
}//class
Saída 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 principal executa-se corretamente numa thread
- podemos aceder às propriedades desta thread através de Thread.currentThread()
- o papel do método sleep. Aqui, a thread que executa a função main fica em espera durante 1 segundo entre cada exibição.
7.2. Criação de threads de execução
É possível ter aplicações em que partes do código são executadas «simultaneamente» em diferentes threads de execução. Quando dizemos que as threads são executadas simultaneamente, estamos frequentemente a usar o termo de forma imprecisa. Se a máquina tiver apenas um processador, como ainda é frequentemente o caso, as threads partilham esse processador: cada uma tem acesso a ele, por sua vez, durante um breve momento (alguns milissegundos). É isto que cria a ilusão de execução paralela. O tempo alocado a uma thread depende de vários fatores, incluindo a sua prioridade, que tem um valor padrão, mas também pode ser definida programaticamente. Quando uma thread tem o processador, normalmente usa-o durante todo o tempo que lhe foi atribuído. No entanto, pode libertá-lo mais cedo:
- aguardando um evento (wait, join)
- entrando em modo de espera por um período especificado (sleep)
- Uma thread T pode ser criada de várias maneiras
- estendendo a classe Thread e sobrescrevendo o seu método run.
- 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 seguinte, os threads são criados utilizando uma classe anónima que estende a classe Thread:
O método *run aqui limita-se a chamar o método *display.
- A execução da thread T é iniciada por T.start(): este método pertence à classe Thread e realiza uma série de inicializações antes de invocar automaticamente o método run da thread ou da interface Runnable. O programa que executa a instrução T.start() não aguarda que a tarefa T termine: passa imediatamente para a instrução seguinte. Temos então duas tarefas a decorrer em paralelo. Estas precisam frequentemente de se comunicar entre si para conhecer o estado do trabalho partilhado a realizar. Este é o problema da sincronização de threads.
- Uma vez iniciado, o thread executa-se de forma autónoma. Ele irá parar quando a função run que está a executar tiver concluído o seu trabalho.
- Podemos esperar que a thread T termine a execução utilizando T.join(). Esta é uma instrução de bloqueio: o programa que a executa fica bloqueado até que a tarefa T tenha concluído o seu trabalho. É também um meio de sincronização.
Vamos examinar o seguinte programa:
// use of threads
import java.io.*;
import java.util.*;
public class thread2{
public static void main(String[] arg) {
// init current thread
Thread main=Thread.currentThread();
// give a name to the current thread
main.setName("myMainThread");
// start of hand
System.out.println("début du thread " +main.getName());
// creation of execution threads
Thread[] tâches=new Thread[5];
for(int i=0;i<tâches.length;i++){
// create thread i
tâches[i]=new Thread() {
public void run() {
affiche();
}
};//def tasks[i]
// set the thread name
tâches[i].setName(""+i);
// start execution of thread i
tâches[i].start();
}//for
// end of hand
System.out.println("fin du thread " +main.getName());
}//Main
public static void affiche() {
// time recovery
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// display start of execution
System.out.println("Début d'exécution de la méthode affiche dans le Thread " +
Thread.currentThread().getName()+ " : " + H);
// sleep for 1 s
try{
Thread.sleep(1000);
}catch (Exception ex){}
// time recovery
calendrier=Calendar.getInstance();
H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// display end of run
System.out.println("Fin d'exécution de la méthode affiche dans le Thread "
+Thread.currentThread().getName()+ " : " + H);
}// poster
}//class
O thread principal, que executa a função main, cria 5 outros threads responsáveis pela execução do método estático display. 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 informativos:
- Em primeiro lugar, verificamos que o início da execução de um thread não é bloqueante. O método principal iniciou a execução de 5 threads em paralelo e terminou a execução antes deles. A operação
inicia a execução da thread tasks[i], mas assim que isso é feito, a execução continua imediatamente com a instrução seguinte, sem esperar que a thread termine.
- Todas as threads criadas devem executar o método display. A ordem de execução é imprevisível. Embora no exemplo a ordem de execução pareça seguir a ordem em que as threads foram iniciadas, não se podem tirar conclusões gerais a partir disso. O sistema operativo aqui tem 6 threads e um processador. Ele irá alocar o processador a estas 6 threads de acordo com as suas próprias regras.
- Os resultados mostram um efeito do método sleep. No exemplo, a thread 0 é a primeira a executar o método display. A mensagem de início de execução é exibida, depois executa o método sleep, que a suspende por 1 segundo. Em seguida, perde o processador, que fica disponível para outra thread. O exemplo mostra que a thread 1 irá obtê-lo. A thread 1 seguirá o mesmo caminho que as outras threads. Quando o período de suspensão de 1 segundo da thread 0 terminar, a sua execução pode ser retomada. O sistema concede-lhe o processador e ela pode concluir a execução do método display.
Vamos modificar o nosso programa para terminar o método *main* com as seguintes instruções:
// end of hand
System.out.println("fin du thread " +main.getName());
// application shutdown
System.exit(0);
A execução do novo programa produz:
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 principal executa a instrução:
Ele interrompe todas as threads da aplicação, não apenas a thread principal. O método principal pode querer aguardar que as threads que criou terminem de ser executadas antes de se encerrar. Isto pode ser feito utilizando o método join da classe Thread:
// waiting for all threads
for(int i=0;i<tâches.length;i++){
// we wait for thread i
tâches[i].join();
}//for
// end of hand
System.out.println("fin du thread " +main.getName());
// application shutdown
System.exit(0);
Isto produz 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. As vantagens dos threads
Agora que destacámos a existência de um thread padrão — aquele que executa o método Main — e sabemos como criar outros, vamos considerar os benefícios dos threads para nós e por que os estamos a apresentar aqui. Existe um tipo de aplicação que se presta bem ao uso de threads: as aplicações cliente-servidor na Internet. Numa aplicação deste tipo, um servidor localizado na máquina S1 responde a pedidos de clientes localizados em máquinas remotas C1, C2, ..., Cn.

Utilizamos aplicações da Internet que seguem este padrão todos os dias: serviços web, e-mail, navegação em fóruns, transferências de ficheiros... No diagrama acima, o servidor S1 deve atender os clientes C1, C2, ..., Cn simultaneamente. Se tomarmos o exemplo de um servidor FTP (File Transfer Protocol) que entrega ficheiros aos seus clientes, sabemos que uma transferência de ficheiros pode, por vezes, demorar várias horas. É, evidentemente, impensável que um único cliente monopolize o servidor durante um período tão longo. O que se faz normalmente é o servidor criar tantos threads de execução quantos forem os clientes. Cada thread é então responsável por lidar com um cliente específico. Uma vez que o processador é partilhado ciclicamente entre todos os threads ativos na máquina, o servidor dedica um pouco de tempo a cada cliente, garantindo assim a concorrência do serviço.

7.4. Uma aplicação de relógio gráfico
Considere 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, um processo deve atualizar a hora a cada segundo. Ao mesmo tempo, os eventos que ocorrem na janela devem ser monitorizados: quando o utilizador clica no botão «Parar», o relógio deve ser parado. Aqui temos duas tarefas paralelas e assíncronas: o utilizador pode clicar a qualquer momento.
Considere o momento em que o relógio ainda não foi iniciado e o utilizador clica no botão «Iniciar». Este é um evento clássico, e poder-se-ia pensar que um método da thread em que a janela está a ser executada poderia então gerir o relógio. No entanto, quando um método da aplicação GUI está a ser executado, a sua thread já não está a ouvir eventos da GUI. Estes eventos ocorrem e são colocados numa fila para serem processados pela aplicação assim que o método atualmente em execução terminar. No nosso exemplo do relógio, o método estará sempre a ser executado, uma vez que apenas clicar no botão «Parar» o pode parar. No entanto, este evento só será processado assim que o método terminar. Estamos presos num ciclo vicioso.
A solução para este problema seria que, quando o utilizador clica no botão «Iniciar», é lançada uma tarefa para gerir o relógio, mas a aplicação pode continuar a ouvir os eventos que ocorrem na janela. Teríamos então duas tarefas separadas a ser executadas em paralelo:
- gestão do relógio
- escuta de eventos da janela
Voltemos ao nosso relógio gráfico:
![]() |
|
O código-fonte da aplicação criada com o 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();
// instance attributes
boolean finHorloge=true;
//Building the frame
public interfaceHorloge() {
enableEvents(AWTEvent.WINDOW_EVENT_MASK);
try {
jbInit();
}
catch(Exception e) {
e.printStackTrace();
}
}
private void runHorloge(){
// we don't stop until we're told to stop
while( ! finHorloge){
// time recovery
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// it is displayed in the T
txtHorloge.setText(H);
// waiting for a second
try{
Thread.sleep(1000);
} catch (Exception e){
// output with error
System.exit(1);
}//try
}// while
}// runHorloge
//Initialize component
private void jbInit() throws Exception {
...................
}
//Replaced, so we can get out when the window is closed
protected void processWindowEvent(WindowEvent e) {
.............
}
void btnGoStop_actionPerformed(ActionEvent e) {
// start/stop clock
// retrieve the button label
String libellé=btnGoStop.getText();
// launch?
if(libellé.equals("Lancer")){
// create the thread in which the clock will run
Thread thHorloge=new Thread(){
public void run(){
runHorloge();
}
};//def thread
// authorize the thread to start
finHorloge=false;
// change the button label
btnGoStop.setText("Arrêter");
// start the thread
thHorloge.start();
// end
return;
}//if
// stop
if(libellé.equals("Arrêter")){
// the thread is told to stop
finHorloge=true;
// change the button label
btnGoStop.setText("Lancer");
// end
return;
}//if
}
}
Quando o utilizador clica no botão «Iniciar», é criada uma thread utilizando uma classe anónima:
O método run da thread chama o método runHorloge da aplicação. Assim que isto é feito, a thread é iniciada:
O método runHorloge será então executado:
private void runHorloge(){
// we don't stop until we're told to stop
while( ! finHorloge){
// time recovery
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// it is displayed in the T
txtHorloge.setText(H);
// waiting for a second
try{
Thread.sleep(1000);
} catch (Exception e){
// output with error
System.exit(1);
}//try
}// while
}// runHorloge
O princípio do método é o seguinte:
- exibe a hora atual na caixa de texto txtHorloge
- faz uma pausa de 1 segundo
- retoma o passo 1, após verificar a variável booleana finHorloge, que será definida como true quando o utilizador clicar no botão Stop.
7.5. Um applet de relógio « »
Convertemos a aplicação gráfica anterior num applet utilizando o método padrão 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 este documento diretamente no IE clicando duas vezes nele, obtemos a seguinte visualização:

Neste exemplo, todos os elementos necessários ao applet estão 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. Mencionámos que, quando o applet é carregado, o método init é executado, seguido do método start, caso este exista. Além disso, quando o utilizador sai da página, o método stop é executado, caso este exista. Quando o utilizador regressa à página do applet, o método start é chamado novamente. Quando um applet implementa threads de animação visual, os métodos start e stop do applet são frequentemente utilizados para iniciar e parar as threads. De facto, é desnecessário que uma thread de animação visual continue a ser executada em segundo plano enquanto a animação está oculta.
Assim, adicionamos os seguintes métodos de início e fim ao nosso applet:
public void stop(){
// the page is hidden
// follow-up
System.out.println("Page stop");
// the page is hidden - stop the thread
finHorloge=true;
}
public void start(){
// the page reappears
// follow-up
System.out.println("Page start");
// restart a new clock thread if necessary
if(btnGoStop.getText().equals("Arrêter")){
// change the wording
btnGoStop.setText("Lancer");
// and act as if the user had clicked on it
btnGoStop_actionPerformed(null);
}//if
}//start
Além disso, adicionámos uma verificação no método run da thread para determinar quando esta começa e termina:
private void runHorloge(){
// follow-up
System.out.println("Thread horloge lancé");
// we don't stop until we're told to stop
while( ! finHorloge){
// time recovery
Calendar calendrier=Calendar.getInstance();
String H=calendrier.get(Calendar.HOUR_OF_DAY)+":"
+calendrier.get(Calendar.MINUTE)+":"
+calendrier.get(Calendar.SECOND);
// it is displayed in the T
txtHorloge.setText(H);
// waiting for a second
try{
Thread.sleep(1000);
} catch (Exception e){
// output with error
return;
}//try
}// while
// follow-up
System.out.println("Thread horloge terminé");
}// runHorloge
Agora executamos o applet com o AppletViewer:
E:\data\serge\Jbuilder\horloge\1>appletviewer appletHorloge.htm
Page start // applet launched - page displayed
Thread horloge lancé // the thread is launched accordingly
Page stop // iconized applet
Thread horloge terminé // the thread is stopped accordingly
Page start // applet redisplayed
Thread horloge lancé // the thread is restarted
Thread horloge terminé // press stop button
Thread horloge lancé // press start button
Page stop // icon applet
Thread horloge terminé // thread arrested accordingly
Page start // applet redisplay
Thread horloge lancé // thread relaunched accordingly
Com o AppletViewer, o evento de início ocorre quando a janela do AppletViewer está visível e o evento de paragem ocorre 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 gerida pela tarefa principal, que definia um valor booleano para parar o segmento do relógio. Vamos agora abordar o problema do acesso simultâneo por tarefas a recursos partilhados, um problema também conhecido como «partilha de recursos». Para ilustrar isto, vamos primeiro examinar um exemplo.
7.6.1. Um contador não sincronizado
Considere a seguinte interface gráfica:

N.º | tipo | nome | função |
1 | JTextField | txtAGenerate | especifica o número de threads a gerar |
2 | JTextField (não editável) | txtGenerated | mostra o número de threads geradas |
3 | JTextField (não editável) | txtStatus | fornece informações sobre erros encontrados e sobre a própria aplicação |
4 | JButton | btnGenerate | inicia a geração de threads |
A aplicação funciona da seguinte forma:
- o utilizador especifica o número de threads a gerar no campo 1
- inicia a geração destas threads utilizando o botão 4
- os threads leem 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. Aqui, pretendemos demonstrar os problemas encontrados numa situação deste tipo. Aqui está um exemplo de execução:

Vemos que solicitámos a geração de 1.000 threads, mas apenas 7 foram contabilizadas. O código relevante para a 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();
// instance variables
Thread[] tâches=null; // threads
int[] compteurs=null; // meters
//Building the frame
public interfaceSynchro() {
..........
}
//Initialize component
private void jbInit() throws Exception {
......................
}
//Replaced, so we can get out when the window is closed
protected void processWindowEvent(WindowEvent e) {
..................
}
void btnGénérer_actionPerformed(ActionEvent e) {
//thread generation
// read the number of threads to generate
int nbThreads=0;
try{
// read the field containing the number of threads
nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
// positive >
if(nbThreads<=0) throw new Exception();
}catch(Exception ex){
//error
txtStatus.setText("Nombre invalide");
// we start again
txtAGénérer.requestFocus();
return;
}//catch
// initially no threads generated
txtGénérés.setText("0"); // task cpteur at 0
// threads are generated and launched
tâches=new Thread[nbThreads];
compteurs=new int[nbThreads];
for(int i=0;i<tâches.length;i++){
// create thread i
tâches[i]=new Thread() {
public void run() {
incrémente();
}
};//thread i
// define its name
tâches[i].setName(""+i);
// start execution
tâches[i].start();
}//for
}//generate
// increment
private void incrémente(){
// retrieve the thread number
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// read the job counter value
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// increment it
compteurs[iThread]++;
// wait 100 milliseconds - the thread will then lose the processor
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// the new counter is displayed
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
// follow-up
System.out.println("Thread " + iThread + " : " + compteurs[iThread]);
}// increment
}// class
Vamos analisar o código:
- A janela declara duas variáveis de instância:
A matriz de tarefas será a matriz de threads geradas. A matriz de contadores será associada à matriz de tarefas. Cada tarefa terá o seu próprio contador para recuperar o valor do campo txtGenerated da GUI.
- Quando o botão Generate é clicado, o método btnGenerate_actionPerformed é executado.
- Este método começa por recuperar o número de threads a gerar. Se necessário, é reportado um erro se este número não for válido. Em seguida, gera as threads solicitadas, tendo o cuidado de registar as suas referências numa matriz e atribuindo um número a cada uma. O método run das threads geradas chama o método increment da classe. Todas as threads são iniciadas (start). A matriz de contadores associada às threads também é criada.
- O método increment:
- lê o valor atual do campo txtGénérés e armazena-o no contador pertencente à thread atualmente em execução
- faz uma pausa de 100 ms para deixar o processador intencionalmente inativo
- exibe o novo valor no campo txtGenerated
Vamos agora explicar por que razão a contagem de threads está incorreta. Suponhamos que há 2 threads para gerar. Estas executam-se numa ordem imprevisível. Uma delas é executada primeiro e lê o valor 0 do contador. Em seguida, define-o para 1, mas não escreve " " na janela: faz uma pausa voluntária de 100 ms. Depois, perde o processador, que é então atribuído a outra thread. Esta thread funciona da mesma forma que a anterior: lê o contador da janela e recupera o 0 que ainda lá está. Define o contador para 1 e, tal como a anterior, faz uma pausa de 100 ms. O processador é então reatribuído à primeira thread: esta thread escreve o valor 1 no contador da janela e termina. O processador é agora atribuído à segunda thread, que também escreve 1. Acabamos por obter um resultado incorreto.
De onde vem o problema? A segunda thread leu um valor incorreto porque a primeira thread foi interrompida antes de terminar a sua tarefa, que era atualizar o contador na janela. Isto leva-nos ao conceito de recurso crítico e secção crítica num programa:
- Um recurso crítico é um recurso que pode ser detido por apenas uma thread de cada vez. Aqui, o recurso crítico é o contador 2 na janela.
- Uma secção crítica de um programa é uma sequência de instruções no fluxo de execução de uma thread durante a qual esta acede a um recurso crítico. Temos de garantir que, durante esta secção crítica, esta é a única com acesso ao recurso.
7.6.2. Contagem sincronizada por método
No exemplo anterior, cada thread executou o método de incremento da janela. O método de incremento foi 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 increment. Considere a seguinte notação:
- o objeto janela F que cria threads em btnGenerate_actionPerformed
- duas threads T1 e T2 criadas por F
Ambas as threads são criadas por F e, em seguida, iniciadas. Portanto, ambas executarão o método F.run. Suponha que T1 chegue primeiro. Ela executa F.run e, em seguida, F.increment, que é um método sincronizado. Ela lê o valor do contador 0, incrementa-o e, em seguida, faz uma pausa de 100 ms. O processador é então transferido para T2, que, por sua vez, executa F.run e, em seguida, F.increment. Neste ponto, fica bloqueado porque a thread T1 está atualmente a executar F.increment, e a palavra-chave synchronized garante que apenas uma thread de cada vez pode executar F.increment. T2 perde então o processador sem ter conseguido ler o valor do contador. Após 100 ms, T1 recupera o processador, exibe o valor do contador 1, sai de F.increment e, em seguida, de F.run, e termina. T2 recupera então o processador e pode agora executar F.incremente porque T1 já não está a executar este método. T2 lê então o valor do contador 1, incrementa-o e faz uma pausa de 100 ms. Após 100 ms, recupera o processador, exibe o valor do contador 2 e também termina. 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 os threads for chamada F, também podemos dizer que o método F.incremente representa um recurso que só deve ser utilizado por um único thread de cada vez. Trata-se, portanto, de um recurso crítico. O acesso sincronizado a este recurso foi assegurado pela palavra-chave synchronized:
Também se poderia dizer que o recurso crítico é o próprio objeto F. Esta restrição é mais rigorosa do que quando o recurso crítico é F.increment. Com efeito, neste último caso, se uma thread T1 executar F.increment, uma thread T2 não pode executar F.increment, mas pode executar outro método do objeto F, quer este seja sincronizado ou não. No caso em que o próprio objeto F é o recurso crítico, quando uma thread T1 executa uma seção sincronizada desse objeto, todas as outras seções sincronizadas do objeto tornam-se inacessíveis para outras threads. Assim, se a thread T1 executar o método sincronizado F.increment, a thread T2 não poderá executar não só F.increment, mas também qualquer outra secção sincronizada de F, mesmo que nenhuma outra thread a esteja a utilizar. Este é, portanto, um método mais restritivo.
Suponhamos, então, que a janela se torne o recurso crítico. Escreveríamos então:
// increment
private void incrémente(){
// review section
synchronized(this){
// retrieve the thread number
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// read the job counter value
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// increment it
compteurs[iThread]++;
// wait 100 milliseconds - the thread will then lose the processor
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// the new counter is displayed
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
}//synchronized
}// increment
Todas as threads utilizam a janela this para sincronizar. Em tempo de execução, obtemos os mesmos resultados corretos de antes. Na verdade, podemos sincronizar em qualquer objeto conhecido por todas as threads. Aqui está outro método, por exemplo, que dá os mesmos resultados:
// instance variables
Thread[] tâches=null; // threads
int[] compteurs=null; // meters
Object synchro=new Object(); // a thread synchronization object
// increment
private void incrémente(){
// review section
synchronized(synchro){
..............
}//synchronized
}// increment
A janela cria um objeto do tipo Object que será utilizado para a sincronização de threads. Este método é melhor do que aquele que sincroniza no objeto this, pois é menos restritivo. Aqui, se uma thread T1 estiver na secção sincronizada de increment e uma thread T2 quiser 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 baseada em eventos
Desta vez, usamos uma variável booleana canPass para indicar a um thread se este pode ou não entrar numa secção crítica. Uma versão sem sincronização poderia ter o seguinte aspeto:
while(! peutPasser); // on attend que peutPasser passe à vrai
peutPasser=false; // aucun autre thread ne doit passer
section critique; // ici le thread est tout seul
peutPasser=true; // un autre thread peut passer dans la section critique
A primeira instrução, em que um thread entra em loop enquanto espera que canPass se torne verdadeiro, é ineficiente: o thread ocupa desnecessariamente o processador. Isto é chamado de espera ativa. Podemos melhorar o código da seguinte forma:
while(! peutPasser){ // wait for peutPasser to change to true
Thread.sleep(100); // off for 100 ms
}
peutPasser=false; // no other thread may pass
section critique; // here the thread is all alone
peutPasser=true; // another thread can enter the critical section
O ciclo de espera é melhor aqui: se a thread não puder passar, ela fica em espera por 100 ms antes de verificar novamente se pode passar ou não. Entretanto, o processador será alocado a outras threads no sistema.
Na verdade, ambos os métodos estão incorretos: não impedem que duas threads entrem na secção crítica ao mesmo tempo. Suponha que uma thread T1 deteta que canPass é verdadeiro. Passará então para a instrução seguinte, onde define canPass como falso para bloquear as outras threads. No entanto, pode muito bem ser preemptada nesse momento, seja porque o seu intervalo de tempo de processador expirou, porque uma tarefa de prioridade mais elevada solicitou o processador, ou por alguma outra razão. O resultado é que perde o processador. Recuperá-lo-á um pouco mais tarde. Entretanto, outras tarefas obterão o processador, incluindo talvez uma thread T2 que entra em loop enquanto espera que peutPasser se torne verdadeiro. Esta também descobrirá que peutPasser é verdadeiro (a primeira thread não teve tempo de o definir como falso) e entrará igualmente na secção crítica. O que não era suposto acontecer.
A sequência
while(! peutPasser){ // wait for peutPasser to change to true
try{
Thread.sleep(100); // off for 100 ms
} catch (Exception e) {}
}// while
peutPasser=false; // no other thread may pass
é uma secção crítica que deve ser protegida por sincronização. Com base no exemplo anterior, podemos escrever:
synchronized(synchro){
while(! peutPasser){ // wait for peutPasser to change to true
try{
Thread.sleep(100); // off for 100 ms
} catch (Exception e) {}
}//while
peutPasser=false; // no other thread may pass
}// synchronized
section critique; // here the thread is all alone
peutPasser=true; // another thread can enter the critical section
Este exemplo funciona corretamente. Podemos melhorá-lo evitando a espera semi-ativa da thread enquanto esta verifica regularmente o valor do booleano canPass. Em vez de acordar a cada 100 ms para verificar o estado de canPass, ela pode entrar em modo de espera e solicitar ser acordada quando canPass for verdadeiro. Escrevemos isto da seguinte forma:
synchronized(synchro){
if (! peutPasser) {
try{
synchro.wait(); // if we can't get through then we wait
} catch (Exception e){
…
}
}
peutPasser=false; // no other thread may pass
}// synchronized
A operação synchro.wait() só pode ser executada por uma thread que seja a atual «proprietária» do objeto synchro. Aqui, trata-se da sequência:
synchronized(synchro){
…
}// synchronized
isso garante que a thread detém o objeto synchro. Através da operação synchro.wait(), a thread cede a posse do bloqueio de sincronização. Porquê? Geralmente porque não dispõe dos recursos necessários para continuar a trabalhar. Assim, em vez de bloquear outras threads que aguardam o recurso synchro, cede-o e aguarda o recurso de que necessita. No nosso exemplo, ela aguarda que o booleano canPass se torne verdadeiro. Como será notificada deste evento? Da seguinte forma:
synchronized(synchro){
if (! peutPasser) {
try{
synchro.wait(); // if we can't get through then we wait
} catch (Exception e){
…
}
}
peutPasser=false; // no other thread may pass
}// synchronized
section critique...
synchronized(synchro){
synchro.notify();
}
Vamos considerar o primeiro thread a adquirir o bloqueio de sincronização. Vamos chamá-lo de T1. Suponhamos que ele encontre o booleano canPass como verdadeiro, uma vez que é o primeiro thread. Em seguida, define-o como falso. Depois, sai da secção crítica bloqueada pelo objeto de sincronização. Outra thread pode então entrar na secção crítica para verificar o valor de canPass. Encontrá-lo-á como falso e aguardará então por um evento (wait). Ao fazê-lo, abdica da posse do objeto sync. Outra thread pode então entrar na secção crítica: também ela aguardará, porque canPass é falso. Podemos, portanto, ter várias threads à espera de um evento no objeto sync.
Voltemos à thread T1, à qual foi concedido acesso. Ela executa a secção crítica e, em seguida, sinaliza que outra thread pode agora prosseguir. Faz-o com a seguinte sequência:
synchronized(synchro){
synchro.notify();
}
Primeiro, deve recuperar a posse do objeto sync utilizando a instrução synchronized. Isto não deverá constituir um problema, uma vez que está a competir com threads que, caso obtenham momentaneamente o objeto sync, têm de o libertar através de um wait, pois verificam que peutPasser é falso. Assim, a nossa thread T1 acabará por adquirir a posse do objeto synchro. Uma vez feito isto, sinaliza através da operação synchro.notify que uma das threads bloqueadas por um synchro.wait deve ser acordada. Em seguida, cede novamente a posse do objeto de sincronização, que é então atribuído a uma das threads em espera. Esta thread continua a sua execução com a instrução seguinte ao wait que a tinha colocado em espera. Por sua vez, irá executar a secção crítica e emitir um synchro.notify para libertar outra thread. E assim sucessivamente.
Vejamos como isto funciona utilizando o exemplo de contagem que já estudámos.
void btnGénérer_actionPerformed(ActionEvent e) {
//thread generation
// read the number of threads to generate
int nbThreads=0;
try{
// read the field containing the number of threads
nbThreads=Integer.parseInt(txtAGénérer.getText().trim());
// positive >
if(nbThreads<=0) throw new Exception();
}catch(Exception ex){
//error
txtStatus.setText("Nombre invalide");
// we start again
txtAGénérer.requestFocus();
return;
}//catch
// RAZ job counter
txtGénérés.setText("0"); // task cpteur at 0
// 1st thread can pass
peutPasser=true;
// threads are generated and launched
tâches=new Thread[nbThreads];
compteurs=new int[nbThreads];
for(int i=0;i<tâches.length;i++){
// create thread i
tâches[i]=new Thread() {
public void run() {
synchronise();
}
};//thread i
// define its name
tâches[i].setName(""+i);
// start execution
tâches[i].start();
}//for
}//generate
Agora, os threads já não executam o método increment, mas sim o seguinte método synchronize:
// thread synchronization step
public void synchronise(){
// request access to the critical section
synchronized(synchro){
try{
// can we get through?
if(! peutPasser){
System.out.println(Thread.currentThread().getName()+ " en attente");
synchro.wait();
}
// we have passed - we forbid other threads to pass
peutPasser=false;
} catch(Exception e){
txtStatus.setText(""+e);
return;
}//try
}// synchronized
// review section
System.out.println(Thread.currentThread().getName()+ " passé");
incrémente();
// we're done - we release any thread blocked at the entrance to the critical section
peutPasser=true;
System.out.println(Thread.currentThread().getName()+ " terminé");
synchronized(synchro){
synchro.notify();
}// synchronized
} // synchronize
O objetivo do método synchronize é processar threads uma de cada vez. Para tal, utiliza uma variável de sincronização chamada synchro. O método increment já não está protegido pela palavra-chave synchronized:
// increment
private void incrémente(){
// retrieve the thread number
int iThread=0;
try{
iThread=Integer.parseInt(Thread.currentThread().getName());
}catch(Exception ex){}
// read the job counter value
try{
compteurs[iThread]=Integer.parseInt(txtGénérés.getText());
} catch (Exception e){}
// increment it
compteurs[iThread]++;
// wait 100 milliseconds - the thread will then lose the processor
try{
Thread.sleep(100);
} catch (Exception e){
System.exit(0);
}
// the new counter is displayed
txtGénérés.setText("");
txtGénérés.setText(""+compteurs[iThread]);
}// increment
Para 5 threads, os resultados 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 as cinco threads geradas pela aplicação. T0 é a primeira a adquirir o bloqueio de sincronização e encontra peutPasser definido como verdadeiro. Define peutPasser como falso e prossegue: este é o objetivo da primeira mensagem enviada. Muito provavelmente, continua e executa a secção crítica, especificamente o método increment. Neste método, ele ficará em espera por 100 ms (sleep). Assim, liberta o processador. O processador é então atribuído a outro thread, o thread T1, que adquire a posse do objeto de sincronização. Ele descobre que não pode prosseguir e entra num estado de espera (wait). Em seguida, liberta a posse do objeto de sincronização, bem como o processador. O processador é atribuído ao thread T2, que sofre o mesmo destino. Durante os 100 ms em que T0 está em pausa, as threads T1 a T4 são, portanto, colocadas em espera. Este é o significado das 4 mensagens «waiting». Após 100 ms, T0 recupera o processador e conclui o seu trabalho: este é o significado da mensagem «0 finished». 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. Aqui, é a T1. A thread T1 entra então na secção crítica: este é o significado da mensagem «1 passed». Ela faz o que precisa de fazer e, em seguida, fica em pausa durante 100 ms. O processador fica então disponível para outra thread, mas todas estão à espera de um evento: nenhuma delas pode ocupar o processador. Após 100 ms, a thread T1 recupera o processador e termina: este é o significado da mensagem «1 finished». As threads T1 a T4 comportar-se-ão da mesma forma que T1: este é o significado das três séries de mensagens: «passed», «finished».



