Você está na página 1de 1

Plataforma

DevMedia

Artigo
8

Invista em você! Saiba como a DevMedia pode ajudar sua carreira. 8

Programação Paralela com


Java
Veja nesse artigo como é possível desenvolver um aplicativo Java que crie Threads, agilizando o processamento

e otimizando o desempenho da CPU utilizando todos seus núcleos.

Utilizamos cookies para fornecer uma melhor experiência para nossos usuários. Para saber mais sobre o uso de cookies,
Aceitar
consulte nossa política de privacidade. Ao continuar navegando em nosso site, você concorda com a nossa política.

Marcar como lido Anotar

Artigos Java Programação Paralela com Java

Nos computadores, o processamento paralelo é o processamento de instruções do programa,


dividindo-as entre vários processadores/núcleos com o objetivo de executar um programa em
menos tempo, conforme mostra a Figura 1.

Figura 1. Exemplo de distribuição de dados entre os núcleos

Nos primeiros computadores, apenas um programa executava de cada vez. A tecnologia evoluiu
permitindo que vários programas executassem “ao mesmo tempo”. Mas isso somente se tornou
possível devido ao avanço dos Sistemas Operacionais que passaram a escalonar as tarefas,
decidindo quem e quando receberia um tempo para executar algo na CPU. Este processo ocorre
tão rápido que aos nossos olhos parece que tudo roda ao mesmo tempo, mas é impossível se
estamos trabalhando em um ambiente com apenas um processador ou núcleo.

As Threads são segmentos de execução de um programa. A Máquina virtual Java (JVM) permite
que um aplicativo possa ter várias threads em execução simultânea. Elas são um segmento

independente da execução de um programa, podendo ser executadas simultaneamente, ou de


forma assíncrona ou síncrona, como podemos ver na Figura 2.

Figura 2. Divisão de tarefas entre as CPUs

Com multithreading adquirimos vários benefícios, tais como:

Elas são levemente comparadas com processos;


Threads compartilham o mesmo espaço de endereço e, portanto, podem compartilhar
dados e código;
Trocas de contexto entre threads são geralmente menos custosas do que entre processos;
Custos de intercomunicação entre as threads são relativamente menores que a dos
processos;
Threads permitem diferentes tarefas a serem realizadas simultaneamente;
Cada Thread possui uma prioridade, que de ne se ela será executada antes ou depois de
outra.

Existem basicamente duas maneiras de se criar threads em Java:

Estendendo a classe Thread ( java.lang.Thread );


Implementando a interface Runnable ( java.lang.Runnable ).

Na Listagem 1 está um exemplo de como criar uma Thread para que execute uma sub tarefa em
paralelo.

1 new Thread(new Runnable() {


2 @Override
3 public void run() {
4 //aqui seu codigo para executar em paralelo
5 }
6 }).start();

Listagem 1. Exemplo de thread implementando a interface Runnable

Estendendo a classe Thread

Na programação Java, threads são instâncias da classe Thread, ou de alguma outra que a
estendeu. Além de serem objetos, threads Java podem executar códigos. Ou seja, assim como na
Listagem 1, para criarmos uma nova thread, basta criar um novo objeto da classe Thread ( new
Thread ) e chamar o método start() deste objeto.

A forma clássica de se criar uma thread é estendendo a classe Thread, assim como na Listagem
2, onde temos a classe Tarefa que estende a classe Thread , que por sua vez implementa a
interface Runnable . Neste caso sobrescrevemos o método run da nova classe, que ca
encarregado de executar nossa tarefa.

1 public class Tarefa extends Thread {


2  
3 private final long valorInicial;
4 private final long valorFinal;
5 private long total = 0;
6  
7 //método construtor que receberá os parametros da tarefa;
8 public Tarefa(int valorInicial, int valorFinal) {
9 this.valorInicial = valorInicial;
10 this.valorFinal = valorFinal;
11 }
12  
13 //método que retorna o total calculado
14 public long getTotal() {
15 return total;
16 }
17  
18 /*
19 Este método se faz necessário para que possamos dar start() na Thread
20 e iniciar a tarefa em paralelo
21 */
22 @Override
23 public void run() {
24 for (long i = valorInicial; i <= valorFinal; i++) {
25 total += i;
26 }
27 }
28 }

Listagem 2. Classe Tarefa estendendo a classe Thread

Para testarmos o paralelismo em nosso programa, criamos a classe ExemploUso com o método
main para executar o programa, conforme a Listagem 3. Por uma questão de melhor
identi cação, toda Thread tem um nome, mesmo não sendo fornecido.

1 public class ExemploUso {


2  
3 public static void main(String[] args) {
4 //cria 3 tarefas
5 Tarefa t1 = new Tarefa(0, 1000);
6 t1.setName("Tarefa1");
7 Tarefa t2 = new Tarefa(1001, 2000);
8 t2.setName("Tarefa2");
9 Tarefa t3 = new Tarefa(2001, 3000);
10 t3.setName("Tarefa3");
11  
12 //inicia a execução paralela das 3 tarefas, iniciando 3 novas threads no programa
13 t1.start();
14 t2.start();
15 t3.start();
16  
17 //aguarda a finalização das tarefas
18 try {
19 t1.join();
20 t2.join();
21 t3.join();
22 } catch (InterruptedException ex) {
23 ex.printStackTrace();
24 }
25  
26 //Exibimos o somatório dos totalizadores de cada Thread
27 System.out.println("Total: " + (t1.getTotal() + t2.getTotal() + t3.getTotal()));
28 }
29 }

Listagem 3. Classe ExemploUso

Em resumo, este programa realiza o somatório de todos os valores em um determinado


intervalo. Em um programa sequencial, se inicia no primeiro valor e a mesma thread vai até seu
último valor.

Neste caso onde usamos multithreading, criamos três threads, subdividindo as tarefas, onde
cada uma recebe um intervalo de valores para somar. No nal cada thread fornece seu
somatório para totalizar com as somas das outras threads. Depois de darmos início às threads,
temos que esperar as mesmas terminarem seu processamento para depois pegarmos o total de
cada uma. Isso é possível através da função join() da thread. A ideia disto é “Dividir para
conquistar”.

Este exemplo foi uma forma simples e prática de paralelizarmos tarefas em um programa, mas
de forma estática, pois a quantidade de threads está pre xada. Seguindo a teoria da
programação concorrente, podemos tornar tudo isso dinâmico, criando a quantidade de threads
ideal para o número de núcleos de processamento existentes no ambiente no qual está sendo
executado o programa. O código a seguir mostra como conseguir esta informação:

1 int nucleos = Runtime.getRuntime().availableProcessors();

Isto é essencial quando estamos falando em otimizar o desempenho, pois precisamos aproveitar
ao máximo os recursos de processamento disponíveis. Criar menos threads do que a quantidade
de núcleos disponíveis gera desperdício. Criar threads exageradamente a mais causa perda de
performance, porque nem todas poderão executar simultaneamente.

Portanto, na Listagem 4 temos uma classe estendendo Thread que ao dar start, procura os
números primos entre um determinado intervalo. Já na Listagem 5 temos a aplicação prática
desta classe, lendo a quantidade de núcleos disponíveis no sistema, criando o número de
threads e dividindo a tarefa entre elas, porém agora com um único intervalo de valores. Cada
thread criada receberá uma faixa de valores para calcular, determinada pelo cálculo de
distribuição de trabalho.

1 import java.util.Collection;
2  
3 public class CalculaPrimos extends Thread {
4  
5 private final int valorInicial;
6 private final int valorFinal;
7 private final Collection<Long> primos;
8  
9 public CalculaPrimos(int valorInicial, int valorFinal, Collection<Long> primos) {
10 this.valorInicial = valorInicial;
11 this.valorFinal = valorFinal;
12 this.primos = primos;
13 }
14  
15 //tarefa a realizar: procurar numeros primos no intervalo recebido
16 @Override
17 public void run() {
18 for (long ate = valorInicial; ate <= valorFinal; ate++) {
19 int primo = 0;
20 for (int i = 2; i < ate; i++) {
21 if ((ate % i) == 0) {
22 primo++;
23 break;
24 }
25 }
26 if (primo == 0) {
27 synchronized (primos) {
28 primos.add(ate);
29 }
30 }
31 }
32 //ao final do trabalho printa o nome de quem terminou
33 System.out.println(this.getName() + " terminou!");
34 }
35 }

Listagem 4. Classe que calcula números primos, estendendo Thread

1 import java.util.ArrayList;
2 import java.util.Collection;
3  
4 public class ExemploUso2 {
5  
6 public static void main(String[] args) {
7 //armazena o tempo inicial
8 long ti = System.currentTimeMillis();
9  
10 //armazena a quantidade de nucleos de processamento disponiveis
11 int numThreads = Runtime.getRuntime().availableProcessors();
12  
13 //intervalo de busca predeterminado
14 int valorInicial = 1;
15 int valorFinal = 1000000;
16  
17 //lista para armazenar os numeros primos encontrados pelas threads
18 Collection<Long> primos = new ArrayList<>();
19  
20 //lista de threads
21 Collection<CalculaPrimos> threads = new ArrayList<>();
22  
23 int trabalho = valorFinal / valorInicial;
24  
25 //cria threads conforme a quantidade de nucleos
26 for (int i = 1; i <= numThreads; i++) {
27 //trab é a quantidade de valores que cada thread irá calcular
28 int trab = Math.round(trabalho / numThreads);
29  
30 //calcula o valor inicial e final do intervalo de cada thread
31 int fim = trab * i;
32 int ini = (fim - trab) + 1;
33  
34 //cria a thread com a classe CalculaPrimos que estende da classe Thread
35 CalculaPrimos thread = new CalculaPrimos(ini, fim, primos);
36 //define um nome para a thread
37 thread.setName("Thread "+i);
38 threads.add(thread);
39 }
40  
41 //percorre as threads criadas iniciando-as
42 for (CalculaPrimos cp : threads) {
43 cp.start();
44 }
45  
46 //aguarda todas as threads finalizarem o processamento
47 for (CalculaPrimos cp : threads) {
48 try {
49 cp.join();
50 } catch (InterruptedException ex) {
51 ex.printStackTrace();
52 }
53 }
54  
55 //imprime os numeros primos encontrados por todas as threads
56 for (Long primo : primos) {
57 System.out.println(primo);
58 }
59  
60 //calcula e imprime o tempo total gasto
61 System.out.println("tempo: " + (System.currentTimeMillis() - ti));
62 }
63 }

Listagem 5. Classe ExemploUso2

Um detalhe importante o qual vale a pena ressaltar é que neste exemplo cada thread recebe a
mesma lista para que adicione os números primos encontrados, e isto gera a famosa
concorrência de dados, onde tenho várias threads concorrendo pelo mesmo objeto. Este é um
fator que se bem trabalhado nos trará bons resultados, mas do contrário, pode ser a ruína do
projeto. Para evitar tais problemas de concorrência, no método run() da Listagem 5, o uso do
objeto primos foi sincronizado, para que somente uma thread por vez o acesse. Claro que tal
prática degrada um pouco a performance, devido a manipulação do lock que as threads devem
fazer antes e depois de acessarem o objeto, mas tudo tem seu custo e este é um que vale a pena
se pagar considerando o benefício obtido.

O ambiente no qual foi desenvolvido este exemplo, possui quatro núcleos e este algoritmo
executou em uma média de tempo de 63 segundos, neste período, enquanto todas as threads
estiverem executando, a CPU deve estar em 100% de operação. Já o algoritmo singlethread da
Listagem 6 executou em 147 segundos e consumindo apenas 25% da CPU. Geralmente o tempo
gasto não diminui proporcionalmente à quantidade de núcleos utilizados, variando muito
conforme a tarefa a ser processada e tempo total consumido, pois este processo se mostra mais
e ciente a medida em que submetemos o algoritmo a rotinas mais exaustivas.

Também é possível observar no console, conforme cada thread vai nalizando seu trabalho, ela
o informa com seu nome. Da mesma maneira, também é possível observar que conforme cada
thread vai nalizando, o uso da CPU vai diminuindo. Neste caso não é coincidência que as
threads vão nalizando na mesma ordem que foram criadas, pois as primeiras threads tem
relativamente menos trabalho que as posteriores, já que receberam números menores para
calcular, logo as primeiras terminam sua tarefa antes das posteriores.

Outro fator importante a ser considerado é de que não podemos sempre contar que cada nova
thread executará em um núcleo diferente. Por exemplo, temos quatro núcleos disponíveis e
criamos quatro threads para fazerem um processamento oneroso, isso não quer dizer que cada
uma delas necessariamente estará sendo executada por um núcleo diferente.

1 import java.util.ArrayList;
2 import java.util.Collection;
3  
4 public class ExemploUso3 {
5  
6 public static void main(String[] args) {
7 //armazena o tempo inicial
8 long ti = System.currentTimeMillis();
9  
10 int valorInicial = 1;
11 int valorFinal = 1000000;
12  
13 //lista para armazenar os numeros primos encontrados pelas threads
14 Collection<Long> primos = new ArrayList<>();
15  
16 //percorre o intervalo buscano os numeros primos
17 for (long ate = valorInicial; ate <= valorFinal; ate++) {
18 int primo = 0;
19 for (int i = 2; i < ate; i++) {
20 if ((ate % i) == 0) {
21 primo++;
22 break;
23 }
24 }
25 if (primo == 0) {
26 synchronized (primos) {
27 primos.add(ate);
28 }
29 }
30 }
31  
32 //imprime os numeros primos encontrados por todas as threads
33 for (Long primo : primos) {
34 System.out.println(primo);
35 }
36  
37 //calcula e imprime o tempo total gasto
38 System.out.println("tempo: "+(System.currentTimeMillis()-ti));
39 }
40 }

Listagem 6. Classe singlethread buscando números primos

Implementando a interface Runnable

A outra forma de criar uma Thread é implementando a interface Runnable em uma classe.
Conforme a Oracle, a interface Runnable deve ser implementado por qualquer classe cujas
instâncias destinam-se a ser executadas por uma Thread. A classe deve de nir um método sem
argumentos chamado run . Esta é a forma mais recomendada pelos pro ssionais Java, por ser,
na maioria dos casos, mais fácil de manipular o que e aonde será executado. Além disso, esta
prática torna mais fácil xar o número de threads simultâneas, bem como a reutilização
daquelas que estão inativas.

Essa interface é projetada para fornecer um protocolo comum para objetos que desejam
executar código enquanto estão ativos. Por exemplo, Runnable é implementado pela classe
Thread. Estar ativo signi ca simplesmente que uma thread foi iniciada e ainda não foi
interrompida.

Além disso, Runnable fornece os meios para uma classe estar ativa sem precisar estender
Thread. Uma classe que implementa Runnable pode ser executada sem estender Thread,
apenas por instanciar uma Thread, passando um Runnable como parâmetro. Na maioria dos
casos, a interface Runnable é indicada se você só necessita substituir o método run() e não há
outros métodos da Thread. Isto é importante porque as classes não devem ser subclasses, a
menos que o programador tem a intenção de modi car ou melhorar o comportamento
fundamental delas.

Na Listagem 7 temos o exemplo de uma classe implementando a interface Runnable. Para não
confundir a explicação, a lógica empregada é a mesma.

1 import java.util.Collection;
2  
3 public class CalculaPrimos2 implements Runnable{
4  
5 private final int valorInicial;
6 private final int valorFinal;
7 private final Collection<Long> primos;
8  
9 public CalculaPrimos2(int valorInicial, int valorFinal,
10 Collection<Long> primos) {
11 this.valorInicial = valorInicial;
12 this.valorFinal = valorFinal;
13 this.primos = primos;
14 }
15  
16 //tarefa a realizar: procurar numeros primos no intervalo recebido
17 @Override
18 public void run() {
19 for (long ate = valorInicial; ate <= valorFinal; ate++) {
20 int primo = 0;
21 for (int i = 2; i < ate; i++) {
22 if ((ate % i) == 0) {
23 primo++;
24 break;
25 }
26 }
27 if (primo == 0) {
28 synchronized (primos) {
29 primos.add(ate);
30 }
31 }
32 }
33 //ao final do trabalho printa o nome de quem terminou
34 System.out.println(Thread.currentThread().getName() + " terminou!");
35 }
36 }

Listagem 7. Classe implementando a interface Runnable

O exemplo de uso também é pouco afetado, a não ser no ponto onde as threads são efetivamente
criadas, conforme a Listagem 8. Agora passamos a criar as threads a partir da própria classe
Thread e apenas informamos como um parâmetro nossa tarefa, que por sua vez, implementa a
interface Runnable .

1 import java.util.ArrayList;
2 import java.util.Collection;
3  
4 public class ExemploUso4 {
5  
6 public static void main(String[] args) {
7 //armazena o tempo inicial
8 long ti = System.currentTimeMillis();
9  
10 //armazena a quantidade de nucleos de processamento disponiveis
11 int numThreads = Runtime.getRuntime().availableProcessors();
12  
13 //intervalo de busca predeterminado
14 int valorInicial = 1;
15 int valorFinal = 1000000;
16  
17 //lista para armazenar os numeros primos encontrados pelas threads
18 Collection<Long> primos = new ArrayList<>();
19  
20 //lista de threads
21 Collection<Thread> threads = new ArrayList<>();
22  
23 int trabalho = valorFinal/valorInicial;
24  
25 //cria threads conforme a quantidade de nucleos
26 for (int i = 1; i <= numThreads; i++) {
27 //trab é a quantidade de valores que cada thread irá calcular
28 int trab = Math.round(trabalho / numThreads);
29  
30 //calcula o valor inicial e final do intervalo de cada thread
31 int fim = trab * i;
32 int ini = (fim - trab) + 1;
33  
34 //cria a thread passando por parametro um objeto da classe CalculaPrimos2
35 que implementa Runnable
36 Thread thread = new Thread(new CalculaPrimos2(ini, fim, primos));
37 //define um nome para a thread
38 thread.setName("Thread "+i);
39 threads.add(thread);
40 }
41  
42 //percorre as threads criadas iniciando-as
43 for (Thread th : threads) {
44 th.start();
45 }
46  
47 //aguarda todas as threads finalizarem o processamento
48 for (Thread th : threads) {
49 try {
50 th.join();
51 } catch (InterruptedException ex) {
52 ex.printStackTrace();
53 }
54 }
55  
56 //imprime os numeros primos encontrados por todas as threads
57 for (Long primo : primos) {
58 System.out.println(primo);
59 }
60  
61 //calcula e imprime o tempo total gasto
62 System.out.println("tempo: "+(System.currentTimeMillis()-ti));
63 }
64 }

Listagem 8. Classe ExemploUso4

O efeito dos exemplos estendendo a classe Thread ou implementando a interface Runnable é o


mesmo, as diferenças consistem na aplicabilidade do código, cabendo a cada desenvolvedor
escolher o qual mais se encaixa em seu contexto.

Nestes exemplos é possível observar o ganho de desempenho utilizando threads, porém existem
outros casos onde o uso de threads se faz necessário, como por exemplo em interfaces grá cas
Swing. Não é possível deixar um JFrame processando algum código e atualizando as
informações da interface grá ca ao mesmo tempo, pois se trata de apenas uma thread que está
fazendo isso. Portanto, nestas situações é necessário que se coloque o código a processar em
uma outra thread para que a thread do JFrame que livre para atualizar os componentes
grá cos.

O paralelismo sem dúvidas é uma ótima prática, que está se dispersando por todos os so wares
do mercado, tendo em vista que atualmente qualquer computador, até mesmo celular tem mais
de um núcleo de processamento. E nosso dever como desenvolvedores é explorar isso, mas
cientes do quão perigoso pode ser.

Visando otimizar ainda mais o desempenho, em alguns casos é possível alocar os núcleos da
GPU para que realizem certas tarefas, mas isso é assunto para um próximo artigo.

Tecnologias:

Java

Marcar como lido Anotar

Por Rodrigo
Em 2015

RECEBA NOSSAS NOVIDADES

Informe o seu e-mail Receber Newsletter

Suporte ao aluno - Deixe a sua dúvida.

ASSINATURA DEVMEDIA
Faça parte dessa comunidade 100% focada em programação e tenha acesso ilimitado.
Nosso compromisso é tornar a sua experiência de estudo cada vez mais dinâmica e
ef iciente. Portanto, se você quer programar de verdade seu lugar é aqui. Junte-se a
69 ,90*
/ MÊS

mais de...
Séries
Projetos completos

800 MIL
Cursos

+ Guias de carreiras

DevCasts

PROGRAMADORES
Desa os

Artigos

App

Suporte em tempo real

Já conhece nosso Plano Empresarial?


Saiba mais

Conheça agora!
A assinatura é cobrada através do seu
cartão de crédito. *Tempo mínimo de
assinatura: 12 meses.

Menu Tecnologias

Revistas Front-End JavaScript


Plataforma para Programadores
Fale conosco .NET Python

Compre pelo WhatsApp:


Trabalhe conosco PHP Mobile
(21) 96759-5390
Assinatura para empresas Java Banco de Dados

Hospedagem web por Porta 80 Web Hosting Delphi Eng. de Software

Av. Ayrton Senna 3000, Shopping Via Parque, grupo 3087 - Barra da Tijuca - Rio de Janeiro - RJ

Você também pode gostar