THREADS EM JAVA Gabriel de Oliveira Ramos <[email protected]> Roland Teodorowitsch <[email protected]> - Orientador Universidade Luterana do Brasil (Ulbra) – Curso de Ciência da Computação – Campus Gravataí Av. Itacolomi, 3.600 – Bairro São Vicente – CEP 94170-240 – Gravataí - RS 15 de novembro de 2009 RESUMO Este artigo apresenta o uso de threads na linguagem de programação Java, descrevendo a classe Thread bem como a interface Runnable. Menciona ainda algumas considerações importantes, como o ciclo de vida de threads, bem como questões sobre prioridades e sincronização de threads. Ao final, mostra um exemplo de aplicativo que utiliza threads bem como uma análise de desempenho de sua execução com e sem threads. Palavras-chave: Thread; Java. ABSTRACT Title: “Java Threads” This paper presents the use of threads in Java programming language, describing the Thread class and the Runnable interface. It mentions too some important considerations about the threads lifecycle and something about threads priorities and synchronization. Finally, shows a thread application example and an analysis of its execution. Keywords: Thread; Java. 1 INTRODUÇÃO O desempenho computacional é um assunto amplamente discutido desde os primórdios da computação. Atualmente, a maioria dos sistemas operacionais trabalha com multiprocessamento para conferir maior desempenho na execução de processos. Em um sistema operacional multiprocessado, algoritmos de escalonamento são utilizados para dividir a utilização da CPU entre os processos em execução. Este mecanismo confere uma resposta muito mais ágil para o usuário uma vez que as tarefas parecem ser executadas ao mesmo tempo. Entretanto, este mecanismo não torna a execução de duas etapas distintas de um processo paralelas, torna paralela apenas a execução dos processos entre si. É neste ponto que se torna necessário o uso de threads. Threads podem ser definidas como fluxos seqüencias de execução de um programa. Logo, um programa pode dividir partes seqüenciais não concorrentes de sua execução em threads distintas, conferindo paralelismo na execução destas. Claro, partes concorrentes podem ser separadas em threads também, entretanto este tipo de procedimento não confere paralelismo, não justificando o uso desta técnica. Este artigo fala sobre o uso de threads na linguagem Java. Neste contexto, este artigo descreve a classe Thread e a interface Runnable, que podem ser utilizadas para se trabalhar com threads em Java. Na seqüencia é abordado o ciclo de vida de uma thread na máquina virtual Java (JVM), prioridades e sincronização de threads. Está presente neste artigo, também, um exemplo de aplicativo que utiliza threads, bem como uma análise do resultado de suas execuções utilizando quantidades variadas de threads. 2 THREADS EM JAVA A máquina virtual Java permite que uma aplicação tenha diversos fluxos seqüências de execução rodando concorrentemente (SUN, 2009a). Para se utilizar esta funcionalidade, a API Java disponibiliza a classe Thread e a interface Runnable. 2.1 A classe Thread A classe Thread é uma classe nativa da linguagem Java, existente desde a API 6.1, que “permite representar um fluxo independente de execução dentro de um programa” (JANDL, 2007, p. 262). Pode-se 1 definir uma thread a partir da criação de uma classe filha de Thread, ou seja, uma subclasse. Uma subclasse de Thread exige a implementação do método run(), que contém o código a ser executado pela thread em si. A Figura 1 mostra um exemplo de código de uma classe filha de Thread que cria aleatoriamente números de 0 a 99, parando apenas ao encontrar o número 50. public class ExemploThread extends Thread { public void run() { int total = 0; while ((int)(Math.random()*100) != 50) total++; System.out.println("Sou a " + this.getName() + " e tentei " + total + " vezes."); } } Figura 1 – Exemplo de subclasse de Thread Para criar uma thread propriamente dita a partir de uma subclasse de Thread, basta declarar um objeto desta, utilizando o operador new para instanciá-la, bem como em qualquer outro tipo de objeto em Java. Instanciado o objeto a thread está pronta para ser rodada. Para isto, deve-se chamar o método start(), responsável por providenciar o seu escalonamento pelo sistema operacional e por executar o método run() (JANDL, 2007, p. 263). A Figura 2 mostra um exemplo deste processo de criação da thread. ExemploThread t = new ExemploThread(); t.start(); Figura 2 – Exemplo de instanciação de um objeto de Thread e sua conseqüente execução Ao executar este código mais de uma vez, o resultado obtido pode ser o mesmo ou não, “refletindo mais o estado do sistema do que da aplicação em si” (JANDL, 2007, p.264). A JVM mantém um programa em execução enquanto suas threads permanecem ativas. De acordo com a Sun (2009a), uma thread é finalizada quando chega ao fim do método run() ou quando gera uma exceção não tratada, fazendo com que esta última seja lançada para o nível superior. 2.2 A interface Runnable Segundo a Sun (2009b), “a linguagem de programação Java não permite herança múltipla”, no entanto há uma alternativa para o uso de mais de uma superclasse, que é a utilização de interfaces. Com base neste conceito, desde a API 6.2 existe a interface Runnable, que possui um funcionamento bem semelhante ao da classe Thread, ao exigir a implementação do método run(). Diferente de implementações que utilizam a classe Thread diretamente, as que utilizam a interface Runnable possuem algumas particularidades. Após instanciar o objeto que possui a interface Runnable, é necessário instanciar também um objeto da classe Thread que receba em seu método construtor uma referência ao objeto da interface Runnable, bem como um nome para a thread criada. Após instanciar este objeto da classe Thread, pode-se iniciar a thread a partir do seu método start(). A Figura 3 mostra um exemplo de uma classe que implementa a interface Runnable, bem como da chamada desta classe a partir da criação de um objeto de Thread. public class ExemploRunnable implements Runnable { public void run() { int total = 0; while ((int)(Math.random()*100) != 50) total++; } } public class Main { public static void main(String[] args) { ExemploRunnable r = new ExemploRunnable(); Thread t = new Thread(r, "Thread 1"); t.start(); } } Figura 3 – Exemplo de utilização de uma interface Runnable 2 Esta interface torna-se muito importante, pois em diversos momentos é necessário que uma classe tenha dois pais ao mesmo tempo. Um bom exemplo deste tipo de aplicação é para interfaces gráficas. Uma classe criada para fazer uma janela, por exemplo, pode ser filha da classe JPanel e, para possibilitar paralelismo e uma melhor experiência para o usuário, pode implementar a classe Runnable. 2.3 Ciclo de vida de uma Thread Uma thread, do momento em que é criada até o qual é finalizada, passa por diversos estados e transições. Este conjunto de estados e transições caracteriza o ciclo de vida de uma thread, o qual pode ser visualizado na Figura 4. Figura 4 – Ciclo de vida de threads Como mencionado anteriormente, as threads são criadas a partir de sua instanciação. Ao disparar o método start(), ela será colocada na lista de threads prontas a serem executadas pelo sistema operacional. Quando o sistema operacional seleciona uma thread para ser executada, ela permanece no estado Execução, produzindo as ações do método run(). Segundo Jandl (2007), uma thread permanece em Execução até que: seu tempo de uso da CPU se esgote ou com a chamada do método yield() quando executa um sleep(), permanecendo no estado Suspenso durante o tempo estabelecido e voltando ao estado Pronto, em seguida; quando executa um wait(), permanecendo no estado Suspenso até que outra thread execute um notify() ou notifyAll(), voltando ao estado Pronto em seguida; quando necessita de uma operação de I/O, ficando no estado Bloqueado até que a operação seja concluída, voltado ao estado Pronto; se a thread finalizar a execução do seu método run() ou quando for executado o método interrupt(), indo para o estado Finalizado. 2.4 Prioridades A prioridade de uma thread corresponde a preferência que ela terá perante as demais durante sua execução. Quanto maior a prioridade de uma thread, maior será sua preferência no uso da CPU. Threads de mesma prioridade costumam partilhar o tempo de CPU igualmente. A prioridade é extremamente ligada ao algoritmo de escalonamento de CPU que o sistema operacional utiliza. Para definir a prioridade de uma thread, são usados números de 1 a 10, sendo que o número 5 é usado para definir a prioridade como normal. Entretanto, nem todos os sistemas operacionais possuem as mesmas prioridades de uma thread Java. Portanto, para garantir que uma thread com prioridade 10 tenha prioridade alta tanto em um sistema operacional cuja prioridade máxima seja 10 quanto em outro que seja 100, a JVM traduz a prioridade especificada no código para a do sistema operacional, logo uma prioridade 10 em Java pode ser traduzida para uma prioridade 100, por exemplo. 3 2.5 Sincronização Durante a execução de threads, há casos em que elas trabalham independentemente uma da outra, sem necessidade de qualquer comunicação entre elas e há casos em que elas comunicam-se de alguma forma ou utilizam dados em comum, denominadas assíncronas e síncronas, respectivamente. Em threads síncronas, geralmente é necessário que a informação que está sendo compartilhada seja consistente, evitando que duas threads mexam nesta ao mesmo tempo, gerando um resultado inconsistente. Para tanto, na linguagem Java há um mecanismo de sincronização, onde esta consistência pode ser garantida. Tal mecanismo pode ser utilizado com a palavra reservada synchronized, aplicada a um método, a um bloco ou mesmo a uma diretiva isolada. Os métodos wait(), notify() e notifyAll() também são muito importantes na sincronização, sendo responsáveis por provocar, respectivamente: uma espera, a liberação de uma ou mais threads em espera (JANDAL, 2007, p. 282). 3 EXEMPLO PRÁTICO Para demonstrar o funcionamento de threads foi feito um aplicativo em Java para simular a utilização da distribuição de uma tarefa entre n elementos processadores, conhecidos na área de sistemas distribuídos como Eps (Elementos Processadores). A tarefa é básica: verificar se os números de um dado intervalo são primos ou não. 3.1 O aplicativo Neste aplicativo foram utilizadas duas classes: a classe EP, filha da classe Thread, que é responsável por realizar a tarefa; e a classe Main, que instancia os objetos da classe EP executando-os em seguida. Inicialmente é definido na classe Main o número de threads a ser utilizado pelo aplicativo e o intervalo da seqüência de números a ser processada. Estes valores são armazenados em variáveis para que sejam utilizados posteriormente pela classe EP. Em seguida, as instâncias da classe EP são criadas, recebendo como parâmetros para seu método construtor: o número da thread, iniciando em 0; o número total de threads, definido anteriormente; o início e o fim do intervalo, também definidos anteriormente. Após instanciadas, as threads têm sua execução iniciada, para que a tarefa comece a ser resolvida. A classe EP utiliza alguns conceitos básicos de computação distribuída. Portanto, foi necessário dividir a seqüência de números entre as threads existentes. Para tal finalidade, uma thread verifica todos os números da seqüência que são múltiplos de seu número, recebido como parâmetro no seu método construtor. A Figura 5 mostra o código-fonte das duas classes mencionadas. public class Main { public static void main(String[] args) { //Variável para determinar o número de threads int nEP = 2; //Variáveis para determinar o intervalo a ser processado int iniS = 2, fimS = 1000; //Instanciação das threads EP ep0 = new EP(0, nEP, iniS, fimS); EP ep1 = new EP(1, nEP, iniS, fimS); //Início da execução das threads ep0.start(); ep1.start(); } } public class EP extends Thread { //Variáveis para guardar o número da thread atual //e o número total de threads, respectivamente private int EPid, nEP; 4 //Variáveis para determinar o intervalo a ser processado private int iniS, fimS; //Variável para controlar se o número é primo ou não private boolean primo; public EP(int EPid, int nEP, int iniS, int fimS) { this.EPid = EPid; this.nEP = nEP; this.iniS = iniS; this.fimS = fimS; } public void run() { //Percorre apenas os númeroes correspondentes à thread, //do intervalo especificado for (int x = EPid + iniS; x <= fimS; x = x + nEP) { //Limpa a variável de controle primo = true; //Verifica se o número é primo for (int y = 2; y < x; y++) if (x % y == 0) primo = false; //Imprime o resultado if (primo) System.out.println("O número " + x + " é primo."); else System.out.println("O número " + x + " não é primo."); } } } Figura 5 – Exemplo de um aplicativo utilizando threads Como pode ser observado na Figura 5, neste caso estão sendo utilizadas apenas duas threads para processar o intervalo de 2 a 1000. 3.2 Os resultados Para demonstrar o desempenho do uso de threads em um aplicativo o código da Figura 5 foi executado utilizando quantidades variadas de threads, com os tempos execução contabilizados, para o intervalo de 2 a 100000 (entre dois e cem mil). Para tal tarefa foi utilizado um computador com processador Intel Core2 Duo que, por possuir dois núcleos físicos, favorece a utilização de threads. O sistema operacional utilizado foi o Windows Vista. A Tabela 1 apresenta os resultados obtidos nas diversas execuções do aplicativo. Tabela 1 – Relação dos tempos de execução, em segundos, do aplicativo Threads utilizadas Tempo para 50000 números Tempo para 100000 números 1 22 76 2 16 54 4 15 54 8 15 49 16 14 50 Como pode ser observado na Tabela 1, a utilização de mais de uma thread para executar uma tarefa traz grandes ganhos. Para um intervalo de cinqüenta mil números, tem-se um ganho de aproximadamente 27% utilizando duas threads e de aproximadamente 32% utilizando oito threads. Já para um intervalo de 5 cem mil números o ganho sobe para quase 29% quando se utilizam duas threads e para quase 36% utilizando oito threads. Percebe-se, portanto, que há um ganho considerável ao utilizar threads em um aplicativo. 4 CONCLUSÃO Este artigo apresentou o uso de threads na linguagem de programação Java, bem como algumas de suas principais características e propriedades, esclarecendo de uma forma simplificada seus conceitos principais. Foi feita também uma análise prática dos benefícios que a utilização de threads em um aplicativo pode trazer. Maiores detalhes e esclarecimentos mais aprofundados sobre o tema podem ser consultados nas referências deste artigo. REFERÊNCIAS JANDL JUNIOR, Peter. Java: guia do programador. São Paulo: Novatec, 2007. 688 p. SUN Microsystems. Thread (Java Platform SE 7 b66). Santa Clara: SUN, 2009. Disponível em: <http://java.sun.com/javase/7/docs/api/java/lang/Thread.html>. Acesso em: 17 out. 2009. SUN Microsystems. Interfaces. Santa Clara: SUN, 2009. Disponível em: <http://java.sun.com/docs/books/tutorial/java/IandI/createinterface.html>. Acesso em: 18 out. 2009. 6