Informações

Tipo: Tutorial
Data de Publicação: 01/01/2004
Revisado em: 01/01/2004

Vote!

  • Currently 4,5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Tags Relacionadas

certificacao introducao

Comentários ( 1 )

Imprimir

Threads - Parte 1

por:

Antônio Theóphilo (actcj@yahoo.com.br)

Threads são linhas de execução que, concorrentemente, dividem o mesmo contexto. A linguagem Java, desde as suas primeiras versões, já oferece suporte nativo a programação com Threads. Para um candidato a prova de certificação é importante entender como funciona o mecanismo de criação, controle e comunicação entre threads na Linguagem Java. Este módulo, o primeiro de uma série de 3 a respeito de Threads, discute o funcionamento básico de Threads em Java.

Assume-se que o leitor tem familiaridade com a Linguagem Java e um conhecimento básico sobre threads. A sugestão para aqueles leitores iniciantes em Threads é ler o artigo introdutório sobre Threads.

Fundamentos

Thread ou linha de execução é uma valiosa ferramenta para o desenvolvimento de sistemas computacionais pois permite que tarefas não correlatas possam ser projetadas e executadas em paralelo. A linguagem Java, além de permitir a criação de várias linhas de execução, também fornece mecanismos de controle de execução e comunicação de threads.

O suporte da Linguagem Java a threads reside em três locais:

  • A classe java.lang.Thread
  • A classe java.lang.Object
  • A Linguagem Java e a JVM (Java Virtual Machine - Máquina Virtual Java)

Uma grande parte dos mecanismos de suporte a threads são implementados na classe java.lang.Thread. Toda thread em Java é representada por um objeto de alguma subclasse de java.lang.Thread (lembre-se que conceitualmente uma classe é subclasse dela mesma). Uma thread pode assumir diversos estados. Ela pode estar sendo executada, pode estar bloqueada esperando por recursos, pode estar morta, dormindo, etc... (os estados de uma thread serão discutidos durante esses módulos).

É importante que um programador Java esteja apto a responder, basicamente, três perguntas:

  • Quando uma thread tem acesso ao processador, que código ela executa?
  • Quais são os estados que uma thread pode assumir?
  • Como ocorre a mudança de estado de uma thread?

A seguir discutiremos as formas de criação de uma thread em Java. Ao final desse texto, o leitor estará preparado para responder a primeira dessas três perguntas.



Criação de threads

Como já foi dito anteriormente toda thread em Java é representada por algum objeto de alguma subclasse de java.lang.Thread. Uma thread é posta para executar quando o método public void start() da classe java.lang.Thread é invocado. Este método registra a thread em uma parte da JVM chamada escalonador de threads. Esse escalonador é responsável por controlar, em última instância, a execução das diversas threads ativas na JVM. O escalonador pode ser parte da JVM ou do próprio sistema operacional, a depender da plataforma/implementação da JVM.

O importante é saber que, ao chamar o método start, não garantimos que o código da thread será executado imediatamente. Veremos posteriormente que isso não garante sequer que a thread será executada alguma vez. O que este método faz é registrar a thread no ambiente de execução. O momento em que a mesma será executada é um aspecto que dependerá da plataforma utilizada e do algoritmo de escalonamento utilizado. A especificação da linguagem não define isso. A escolha fica a cargo da implementação da JVM. O que temos garantia é que, uma vez registrada, a thread esperará a sua vez de ter acesso ao processador para executar suas atividades.

Agora temos condição de responder a primeira daquelas três perguntas anteriores: Quando uma thread tem acesso ao processador, que código ela executa?
R: Quando o escalonador de threads autoriza uma determinada thread a utilizar o processador pela primeira vez, é executado o codigo contido no método public void run(). Caso esta thread seja interrompida pelo escalonador para dar chance de outra thread ser executada, a mesma ficará aguardando a sua vez e quando esta última chegar a execução novamente, será reiniciada exatamente do ponto aonde se interrompeu a execução anterior, e não do ínicio do método public void run().

A thread pode executar o seu próprio método run ou o método run de algum outro objeto. Este fato é a base para as duas formas existentes de criação de threads:

  • Através de uma subclasse da classe java.lang.Thread
  • Através de uma classe que implemente a interface java.lang.Runnable

A primeira forma é a mais simples de todas e consiste em criar uma classe que herde de java.lang.Thread. Uma vez criada esta classe, basta criar um objeto da mesma e chamar o método start. Este método registrará a thread no escalonador. Não é obrigatório que a subclasse em questão defina o método run. A classe Thread provê uma implementaçao para o mesmo. No entanto esta implementação não faz nada. Logo, é natural que a classe em questão sobrescreva este método a fim de colocar ali o código que deseja ser executado.

Importante: Deve-se sobrescrever o método public void run() e não o método public void start() pois caso a subclasse sobrescreva este último a thread não será criada na JVM e não será registrada no escalonador. Abaixo está um exemplo de criação de uma thread utilizando a estratégia de herdar da classe java.lang.Thread:

01. public class MinhaThread extends Thread {
02.    
03.    private String msg;
04.    
05.    public MinhaThread(String msg) {
06.       this.msg = msg;
07.    }
08.    
09.    public void run() {
10.       while(true) {
11.          System.out.println(msg);
12.       }
13.    }
14.    
15.    public static void main(String[] args) {
16.       MinhaThread t = new MinhaThread("Thread criada sendo executada");
17.       t.start();
18.       while(true) {
19.          System.out.println("Thread principal sendo executada");
20.       }
21.    }
22. }

O código acima define uma classe chamada MinhaThread que herda de java.lang.Thread e que sobrescreve o método run. O método main cria um objeto da classe MinhaThread e chama o método start definido na superclasse. Este método cria uma nova thread que irá executar o método run do próprio objeto assim que o escalonador da JVM decidir. A execução concorrente da thread criada (representada pelo objeto t) com a thread principal (thread que invocou o método main) pode ser facilmente vista através dos dois laços existentes no código. O laço definido nas linhas 10-12 será executado pela thread que foi criada na linha 16 e registrada na linha 17. O laço definido nas linhas 18-20 será executado pela thread principal concorrentemente com o laço anterior.

O leitor pode estar perguntando se o mesmo efeito não seria conseguido de maneira mais simples se ao invés de chamar o método start na linha 17 se chamasse o método run. O que aconteceria é que o método run é um método como outro qualquer e portanto ao ser executado não seria criada uma outra thread de execução (tarefa realizada pelo método start da classe java.lang.Thread), ou seja, haveria uma única linha de execução que nunca executaria o laço das linhas 18-20 pois estaria sempre executando o laço das linhas 10-12.

O código acima pode ser encontrado pronto para compilação aqui Download MinhaThread.java.

A segunda forma de se criar uma thread é fazendo-a executar o método public void run() de algum outro objeto que não pertence à árvore de herança da classe java.lang.Thread. Dissemos anteriormente que toda thread é representada por um objeto de alguma subclasse de java.lang.Thread, então como é possível para uma thread executar o código de um objeto de uma outra classe? Isso é implementado através do construtor Thread(Runnable r) da classe Thread. Este construtor recebe como parâmetro uma referência a um objeto que implementa a interface java.lang.Runnable, uma interface muito simples que declara um único método: public void run(). Ao se chamar este construtor a referência passada como argumento é armazenada e ao ser invocado o método start() da thread ela registra a thread junto ao escalonador (até aqui o comportamento é identico ao caso anterior). Contundo, quando a thread obtém o processador, o método run do objeto que implementa a interface Runnable é executado (não o método run do objeto da classe Thread).

Abaixo está um exemplo de utilização desta técnica com o mesmo efeito do exemplo anterior:

01. class Imprime implements Runnable {
02.    
03.    private String msg;
04.    
05.    Imprime(String msg) {
06.       this.msg = msg;
07.    }
08.    
09.    public void run() {
10.       while(true) {
11.          System.out.println(msg);
12.       }
13.    }
14. }
15. 
16. public class MinhaThread2 {
17.    public static void main(String[] args) {
18.       Imprime i = new Imprime("Thread criada sendo executada");
19.       Thread t = new Thread(i);
20.       t.start();
21.       while(true) {
22.          System.out.println("Thread principal sendo executada");
23.       }
24.    }
25. }

O código acima pode ser encontrado pronto para compilação aqui Download MinhaThread2.java.

A diferença deste código para o anterior é que não mais utilizamos uma subclasse de java.lang.Thread. Ao invés disso, utilizamos um objeto da própria classe java.lang.Thread. No entanto nós definimos, através do construtor da classe, que código a thread deveria executar. O objeto que é passado como argumento para o construtor deve implementar a interface Runnable. Dessa forma, a thread irá chamar o método run do objeto passado como parâmetro no momento que ela for autorizada a utilizar o processador. A classe Imprime implementa a interface Runnable. Se tivéssemos utilizado o construtor vazio da classe Thread, o código compilaria normalmente. Mas ao invés de executar o método run da classe Imprime, a thread executaria o método run da classe Thread (que não faz nada).

O leitor pode estar achando que esta segunda alternativa é muito mais complicada e não teria muito sentido utilizá-la já que a primeira opção faria o mesmo de maneira mais simples. A vantagem desta segunda abordagem é um pouco mais sutil. Java não permite herança múltipla. Caso a classe que contém o método run extenda Thread, não será possível herdar o comportamento de outra classe. Outro aspecto favorável em se utilizar a segunda estratégia é que, olhando sob o ponto de vista de orientação a objetos, herdando da classe Thread e colocando nela o método run o desenvolvedor acaba relacionando duas funcionalidades diferentes que na maioria das vezes não são correlatas: o suporte à programação multi-thread provido pela classe Thread e o código a ser executado. Utilizando a estratégia de se usar classes que herdam da classe Thread o implementador está dizendo que a sua classe é uma thread enquanto que utilizando a estratégia de implementar a interface Runnable do ponto de vista de orientação a objetos o desenvolvedor está dizendo que a sua classe não é uma Thread mas que está associada a uma Thread, o que é mais coerente.

Ao Terminar a Execução

Ao terminar a execução de uma thread, ou seja quando o método run é finalizado, a thread é considerada morta não podendo portanto ser reiniciada. O objeto que representa a thread ainda existe logo pode-se invocar qualquer um de seus métodos com exceção do método start. Caso se chame este método em uma thread morta uma exceção em tempo de execução será lançada indicando que a thread está em um estado inválido para aquela operação (iniciação).

Pode-se usar um mesmo objeto que implemente a interface Runnable e submetê-lo à objetos Thread quantas vezes forem necessárias, desde que as threads que são representadas por estes objetos não estejam mortas.

Obs.: Existe um método chamado stop() que pode ser chamado por outra thread e finaliza a execução da primeira. Este método está depreciado (deprecated) desde o JDK 1.2 e deve ser substituído pelo método interrupt(). O exame de programador não lhe irá avaliar sobre este método depreciado.

Estados de uma Thread

Durante todo o ciclo de vida de uma thread a mesma pode assumir alguns estados (veja figura 1). Dois desses estados destacamos como sendo os principais: o estado pronto e o estado executando. Uma thread está no estado "executando" quando a mesma é escolhida pelo escalonador e obtém o direito de usar o processador. Numa máquina monoprocessada (com apenas um único processador) apenas uma thread pode estar neste estado num dado momento. O estado "pronto" determina as threads que estão prontas para executar e estão aguardando a sua vez de serem escolhidas pelo escalonador. Quando o método start de uma thread é invocado, a thread é registrada junto ao escalonador que coloca a mesma no estado "pronto". A partir daí, a thread vai disputar com outras o direito de assumir o controle do processador. Que critério o escalonador vai usar para escolher que thread terá o controle do processador é algo que vai depender da implementação da JVM. A especificação da linguagem Java não determina como esse algoritmo de escalonamento deve funcionar.

Figura 1: Estados "vivos" de uma thread.

O fluxo normal de execução de uma thread é: estar no estado "executando" por um determinado tempo; ir para o estado "pronto" para dar a chance a outra thread de utilizar o procesador e por fim voltar ao estado "executando" por um determinado tempo completando assim um ciclo que irá terminar quando o método run acabar a sua execução. Depois disso, a thread passa ao estado "morto" (não mostrado na figura 1).

Esse seria o fluxo normal de execução, no entanto existem outros estados que a thread pode entrar depois de sair do estado executando e antes de entrar no estado pronto. As próximas seções tratarão desses estados detalhadamente indicando quando uma thread a transição entre esses estados.

Prioridades de uma thread

Uma thread possui um parametro que indica a sua prioridade. Lembre-se que a especificação da lingugem não dita como será o processo de escalonamento das threads e portanto, não determina como esse parâmetro influência na decisão do escalonador. A especificação da linguagem apenas indica que as threads devem possuir prioridades. Geralmente (não é garantido!) o escalonador escolhe a thread que possui a maior prioridade, caso haja mais de uma ele irá escolher uma delas, não necessariamente a que está esperando a mais tempo.

O número que indica a prioridade deve ser um valor entre 1 e 10. Este valor pode ser definido/consultado através dos métodos: setPriority()/getPriority(). Existem ainda as constantes Thread.MAX_PRIORITY, Thread.NORM_PRIORITY e Thread.MIN_PRIORITY para os valores de prioridade máxima(10), normal(5) e mínima(1). É preferível usar estas constantes a fim de deixar o código mais portável e de fácil manutenção.

Uma thread ao ser criada possui a prioridade igual ao da thread que a criou a menos que seja utilizado o método setPriority(). O valor padrão utilizado pela máquina virtual para a prioridade de uma thread é Thread.NORM_PRIORITY (5).

Obs.: Uma última nota a respeito do uso de prioridades em threads: deve-se ter muito cuidado ao se desenvolver código que se baseia no uso que a máquina virtual faz das prioridades para escalonamento das threads visto que a especificação da linguagem não define este uso e deixa esta escolha a cargo da implementação da JVM.

Comentários (1)

Ótimo tutorial. Gostaria apenas de saber se já saiu as outras continuações e se sim, onde. Parabéns!
postado por Reinaldo de Carvalho em 06/12/2007 às 23:21
Comente!

Observações

Os campos em negrito são obrigatórios.

Para evitar problemas, este espaço é moderado. Após o envio do comentário será necessário aguardar pela sua aprovação.