Programação Paralela Com Java
Programação Paralela Com Java
DevMedia
Artigo
8
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.
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
Na Listagem 1 está um exemplo de como criar uma Thread para que execute uma sub tarefa em
paralelo.
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.
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.
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:
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 }
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 }
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 }
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 }
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 }
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
Por Rodrigo
Em 2015
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
Conheça agora!
A assinatura é cobrada através do seu
cartão de crédito. *Tempo mínimo de
assinatura: 12 meses.
Menu Tecnologias
Av. Ayrton Senna 3000, Shopping Via Parque, grupo 3087 - Barra da Tijuca - Rio de Janeiro - RJ