Skip to content

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:

currentThread()
retorna o thread atualmente em execução
setName()
define o nome do thread
getName()
nome do thread
isAlive()
indica se o thread está ativo (true) ou não (false)
start()
inicia a execução de um thread
run()
método executado automaticamente após a execução do método start anterior
sleep(n)
suspende a execução de um thread durante n milissegundos
join()
operação bloqueante — aguarda o fim do thread para passar à instrução seguinte

Os construtores mais utilizados são os seguintes:

Thread()
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.
Thread(Runnable object)
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-se a execução do thread i
            tâches[i].start();

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:

    System.exit(0);

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.

Image

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.

Image

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:

n.º
tipo
nome
função
1
JTextField (Editable=false)
txtHorloge
exibe a hora
2
JButton
btnGoStop
pára ou inicia o relógio

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:

      Thread thHorloge=new Thread(){
        public void run(){
          runHorloge();
        }

O método run da thread remete para o método runHorloge da aplicação. Feito isto, a thread é iniciada:

      // inicia-se o thread
      thHorloge.start();

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:

  1. exibe a hora atual na caixa de texto txtHorloge
  2. pausa durante 1 segundo
  3. 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:

Image

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:

Image

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:

Image

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:

    private void incremente()

Agora, declaramo-lo de forma diferente:

   // incrementar
  private synchronized void incrémente(){

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:

Image

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:

    private synchronized void incrémente()

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».