Em relação aos conceitos de paralelismo de tarefas, considere as afirmações a seguir.

No que se refere às construções machadianas, constatamos que a quebra do paralelismo semântico foi manifestada de forma intencional. Tal propósito se deve aos recursos utilizados pela linguagem literária, no intuito de conferir mais ênfase à mensagem. Dessa forma, temos que no segundo exemplo, o autor conseguiu obter um significativo efeito de estilo, bem como no primeiro, no qual se depreende que o recurso irônico, representando uma de suas marcas, foi utilizado para enfatizar o interesse financeiro de Marcela.

Partindo de tais pressupostos, cumpre dizer que mesmo em se tratando de um “desvio”, no caso da linguagem artística esse não é considerado como tal, em virtude do que chamamos de licença poética.   

Quanto ao terceiro exemplo, infere-se que a quebra também se deu no âmbito semântico – uma vez detectada pela quebra de expectativa por parte do leitor ao fazer a junção entre dois elementos de naturezas distintas: livros e frutas.

Voltar a questão

Voltar

Artigos Java Threads: paralelizando tarefas com os diferentes recursos do Java

Por que eu devo ler este artigo:Este artigo aborda o paralelismo de tarefas em softwares Java, pr�tica de desenvolvimento muito utilizada e necess�ria nos dias de hoje, onde temos arquiteturas computacionais essencialmente paralelas. Os desenvolvedores que desejam otimizar o desempenho de um software encontrar�o neste artigo explica��es sobre o funcionamento das principais APIs multithreaded da plataforma Java, bem como os problemas mais triviais e formas de como evit�-los.

A plataforma Java disponibiliza diversas APIs para implementar o paralelismo desde as suas primeiras vers�es, e estas veem evoluindo at� hoje, trazendo novos recursos e frameworks de alto n�vel que auxiliam na programa��o. No entanto, deve-se lembrar que a tecnologia n�o � tudo. � importante, tamb�m, conhecer os conceitos desse tipo de programa��o e boas pr�ticas no desenvolvimento voltado para esse cen�rio.

O processamento paralelo, ou concorrente, tem como base um hardware multicore, onde disp�e-se de v�rios n�cleos de processamento. Estas arquiteturas, no in�cio do Java, n�o eram t�o comuns. No entanto, atualmente j� se encontram amplamente difundidas, tanto no contexto comercial como dom�stico. Diante disso, para que n�o haja desperd�cio desses recursos de hardware e possamos extrair mais desempenho do software desenvolvido, � recomendado que alguma t�cnica de paralelismo seja utilizada.

Como sabemos, existem diversas formas de criar uma aplica��o que implemente paralelismo, formas estas que se diferem tanto em t�cnicas como em tecnologias empregadas. Em vista disso, no decorrer deste artigo ser�o contextualizadas as principais APIs Java, desde as threads �cl�ssicas� a modernos frameworks de alto n�vel, visando otimizar a constru��o, a qualidade e o desempenho do software.

Arquitetura Multicore

Uma arquitetura multicore consiste em uma CPU que possui mais de um n�cleo de processamento. Este tipo de hardware permite a execu��o de mais de uma tarefa simultaneamente, ao contr�rio das CPUs singlecore, que eram constitu�das por apenas um n�cleo, o que significa, na pr�tica, que nada era executado efetivamente em paralelo.

A partir do momento em que se tornou invi�vel desenvolver CPUs com frequ�ncias (GHz) mais altas, devido ao superaquecimento, partiu-se para outra abordagem: criar CPUs multicore, isto �, inserir v�rios n�cleos no mesmo chip, com a premissa base de dividir para conquistar.

Ao contr�rio do que muitos pensam, no entanto, os processadores multicore n�o somam a capacidade de processamento, e sim possibilitam a divis�o das tarefas entre si. Deste modo, um processador de dois n�cleos com clock de 2.0 GHz n�o equivale a um processador com um n�cleo de 4.0 GHz. A tecnologia multicore simplesmente permite a divis�o de tarefas entre os n�cleos de tal forma que efetivamente se tenha um processamento paralelo e, com isso, seja alcan�ado o t�o almejado ganho de performance.

Contudo, este ganho � poss�vel apenas se o software implementar paralelismo. Neste contexto, os Sistemas Operacionais, h� anos, j� possuem suporte a multicore, mas isso somente otimiza o desempenho do pr�prio SO, o que n�o � suficiente. O ideal � cada software desenvolvido esteja apto a usufruir de todos os recursos de hardware dispon�veis para ele.

Ademais, considerando o fato de que hoje j� nos deparamos com celulares com processadores de quatro ou oito n�cleos, os softwares a eles disponibilizados devem estar preparados para lidar com esta arquitetura. Desde um simples projeto de rob�tica a um software massivamente paralelo para um supercomputador de milh�es de n�cleos, a op��o por paralelizar ou n�o, pode significar a diferen�a entre passar dias processando uma determinada tarefa ou apenas alguns minutos.

Multitasking

O multitasking, ou multitarefa, � a capacidade que sistemas possuem de executar v�rias tarefas ou processos ao mesmo tempo, compartilhando recursos de processamento como a CPU. Esta habilidade permite ao sistema operacional intercalar rapidamente os processos ativos para ocuparem a CPU, dando a impress�o de que est�o sendo executados simultaneamente, conforme a Figura 1.

No caso de uma arquitetura singlecore, � poss�vel executar apenas uma tarefa por vez. Mas com o multitasking esse problema � contornado gerenciando as tarefas a serem executadas atrav�s de uma fila, onde cada uma executa por um determinado tempo na CPU. Nos sistemas operacionais isto se chama escalonamento de processos.

Figura 1. Processos executando em um n�cleo.

Em arquiteturas multicore, efetivamente os processos podem ser executados simultaneamente, conforme a Figura 2, mas ainda depende do escalonamento no sistema operacional, pois geralmente temos mais processos ativos do que n�cleos dispon�veis para processar.

Figura 2. Arquitetura multicore executando processos.

Desta forma, mais n�cleos de processamento significam que mais tarefas simult�neas podem ser desempenhadas. Contudo, vale ressaltar que isto s� � poss�vel se o software que est� sendo executado sobre tal arquitetura implementa o processamento concorrente. De nada adianta um processador de oito n�cleos se o software utiliza apenas um.

Multithreading

De certo modo, podemos compreender multithreading como uma evolu��o do multitasking, mas em n�vel de processo. Ele, basicamente, permite ao software subdividir suas tarefas em trechos de c�digo independentes e capazes de executar em paralelo, chamados de threads. Com isto, cada uma destas tarefas pode ser executada em paralelo caso haja v�rios n�cleos, conforme demonstra a Figura 3.

Figura 3. Processo executando v�rias tarefas.

Diversos benef�cios s�o adquiridos com este recurso, mas, sem d�vida, o mais procurado � o ganho de performance. Al�m deste, no entanto, tamb�m � v�lido destacar o uso mais eficiente da CPU. Sabendo dessa import�ncia, nosso pr�ximo passo � entender o que s�o as threads e como cri�-las para subdividir as tarefas do software.

Threads

Na plataforma Java, as threads s�o, de fato, o �nico mecanismo de concorr�ncia suportado. De forma simples, podemos entender esse recurso como trechos de c�digo que operam independentemente da sequ�ncia de execu��o principal. Como diferencial, enquanto os processos de software n�o dividem um mesmo espa�o de mem�ria, as threads, sim, e isso lhes permite compartilhar dados e informa��es dentro do contexto do software.

Cada objeto de thread possui um identificador �nico e inalter�vel, um nome, uma prioridade, um estado, um gerenciador de exce��es, um espa�o para armazenamento local e uma s�rie de estruturas utilizadas pela JVM e pelo sistema operacional, salvando seu contexto enquanto ela permanece pausada pelo escalonador.

Na JVM, as threads s�o escalonadas de forma preemptiva seguindo a metodologia �round-robin�. Isso quer dizer que o escalonador pode paus�-las e dar espa�o e tempo para outra thread ser executada, conforme a Figura 4. O tempo que cada thread recebe para processar se d� conforme a prioridade que ela possui, ou seja, threads com prioridade mais alta ganham mais tempo para processar e s�o escalonadas com mais frequ�ncia do que as outras.

Figura 4. Escalonamento de threads, modo round-robin.

Tamb�m � poss�vel observar na Figura 4 que apenas uma thread � executada por vez. Isto normalmente acontece em casos onde s� h� um n�cleo de processamento, o software implementa um sincronismo de threads que n�o as permite executar em paralelo ou quando o sistema n�o faz uso de threads. Na Figura 5, por outro lado, temos um cen�rio bem diferente, com v�rias threads executando paralelamente e otimizando o uso da CPU.

Figura 5. Escalonamento de threads no modo round-robin implementando paralelismo.

Desde seu in�cio a plataforma Java foi projetada para suportar programa��o concorrente. De l� para c�, principalmente a partir da vers�o 5, foram inclu�das APIs de alto n�vel que nos fornecem cada vez mais recursos para a implementa��o de tarefas paralelas, como as APIs presentes nos pacotes java.util.concurrent.*.

Saiba que toda aplica��o Java possui, no m�nimo, uma thread. Esta � criada e iniciada pela JVM quando iniciamos a aplica��o e sua tarefa � executar o m�todo main() da classe principal. Ela, portanto, executar� sequencialmente os c�digos contidos neste m�todo at� que termine, quando a thread encerrar� seu processamento e a aplica��o poder� ser finalizada.

Em Java, existem basicamente duas maneiras de criar threads:

  • Estender a classe Thread (java.lang.Thread); e
  • Implementar a interface Runnable (java.lang.Runnable).

Na Listagem 1, de forma simples e objetiva, � apresentado um exemplo de como implementar uma Thread para executar uma subtarefa em paralelo. Para isso, primeiramente � necess�rio codificar um Runnable, o que pode ser feito diretamente na cria��o da Thread, como demonstrado na Listagem 1, ou implementar uma classe pr�pria que estenda Runnable. Posteriormente, basta execut�-lo com um objeto Thread atrav�s do m�todo start().

Listagem 1. Exemplo de thread implementando a interface Runnable.

public class ExemploThread { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { //c�digo para executar em paralelo System.out.println("ID: " + Thread.currentThread().getId()); System.out.println("Nome: " + Thread.currentThread().getName()); System.out.println("Prioridade: " + Thread.currentThread().getPriority()); System.out.println("Estado: " + Thread.currentThread().getState()); } }).start(); } }

Neste exemplo pode-se observar tamb�m o c�digo utilizado para buscar alguns dados da thread atual, tais como ID, nome, prioridade, estado e at� mesmo capturar o c�digo que ela est� executando. Al�m de tais informa��es que podem ser capturadas, � poss�vel manipular as threads utilizando alguns dos seguintes m�todos:

  • O m�todo est�tico Thread.sleep(), por exemplo, faz com que a thread em execu��o espere por um per�odo de tempo sem consumir muito (ou possivelmente nenhum) tempo de CPU;
  • O m�todo join() congela a execu��o da thread corrente e aguarda a conclus�o da thread na qual esse m�todo foi invocado;
  • J� o m�todo wait() faz a thread aguardar at� que outra invoque o m�todo notify() ou notifyAll(); e
  • O m�todo interrupt() acorda uma thread que est� dormindo devido a uma opera��o de sleep() ou wait(), ou foi bloqueada por causa de um processamento longo de I/O.

A forma cl�ssica de se criar uma thread � estendendo a classe Thread, como demonstrado na Listagem 2. Neste c�digo, temos a classe Tarefa estendendo a Thread. A partir disso, basta sobrescrever o m�todo run(), o qual fica encarregado de executar o c�digo da thread.

Na pr�tica, nossa classe Tarefa � respons�vel por realizar o somat�rio do intervalo de valores recebido no momento em que ela � criada e armazen�-lo em uma vari�vel para que possa ser lido posteriormente.

Listagem 2. C�digo da classe Tarefa estendendo a classe Thread.

public class Tarefa extends Thread { private final long valorInicial; private final long valorFinal; private long total = 0; //m�todo construtor que receber� os par�metros da tarefa public Tarefa(int valorInicial, int valorFinal) { this.valorInicial = valorInicial; this.valorFinal = valorFinal; } //m�todo que retorna o total calculado public long getTotal() { return total; } /* Este m�todo se faz necess�rio para que possamos dar start() na Thread e iniciar a tarefa em paralelo */ @Override public void run() { for (long i = valorInicial; i <= valorFinal; i++) { total += i; } } }

Listagem 3. C�digo da classe Exemplo, utiliza a classe Tarefa.

public class Exemplo { public static void main(String[] args) { //cria tr�s tarefas Tarefa t1 = new Tarefa(0, 1000); t1.setName("Tarefa1"); Tarefa t2 = new Tarefa(1001, 2000); t2.setName("Tarefa2"); Tarefa t3 = new Tarefa(2001, 3000); t3.setName("Tarefa3"); //inicia a execu��o paralela das tr�s tarefas, iniciando tr�s novas threads no programa t1.start(); t2.start(); t3.start(); //aguarda a finaliza��o das tarefas try { t1.join(); t2.join(); t3.join(); } catch (InterruptedException ex) { ex.printStackTrace(); } //Exibimos o somat�rio dos totalizadores de cada Thread System.out.println("Total: " + (t1.getTotal() + t2.getTotal() + t3.getTotal())); } }

Para testarmos o paralelismo com a classe da Listagem 2, criamos a classe Exemplo com o m�todo main(), respons�vel por executar o programa (vide Listagem 3). Neste exemplo, ap�s criar as threads, chama-se o m�todo start() de cada uma delas, para que iniciem suas tarefas. Logo ap�s, em um bloco try-catch, temos a invoca��o dos m�todos join(). Este faz com que o programa aguarde a finaliza��o de cada thread para que depois possa ler o valor totalizado por cada tarefa.

Observe, na Listagem 3, que cada tarefa recebe seu intervalo de valores a calcular, sendo somado, ao todo, de 0 a 3000, mas e se tiv�ssemos uma �nica lista de valores que gostar�amos de somar para obter o valor total? Neste caso, as threads precisariam concorrer pela lista. Isso � o que chamamos de concorr�ncia de dados e geralmente traz consigo diversos problemas.

Concorr�ncia de dados

A concorr�ncia de dados � um dos principais problemas a se enfrentar quando empregamos multithreading em uma aplica��o. Ela � capaz de gerar desde inconsist�ncia nos dados compartilhados at� erros em tempo de execu��o. No entanto, felizmente isto pode ser evitado, sendo necess�rio, portanto, se precaver para que nosso aplicativo n�o apresente tais problemas.

Uma boa forma de evitar problemas de concorr�ncia � sincronizar as threads que compartilham dados entre si. A partir disso, estas threads passam a executar em sincronia com outras, e assim, uma por vez acessar� o recurso. O sincronismo previne que duas ou mais threads acessem o mesmo recurso simultaneamente. Por outro lado, temos as threads ass�ncronas, que executam independentemente umas das outras e geralmente n�o compartilham recursos, como � o caso do exemplo das Listagens 2 e 3.

No exemplo da Listagem 4, por sua vez, � poss�vel visualizar tr�s threads disputando a mesma vari�vel varCompartilhada para increment�-la de forma ass�ncrona. Basicamente, a ideia desse c�digo � incrementar uma vari�vel com diferentes valores e, a cada valor gerado, adicion�-lo em uma lista (ArrayList).

Listagem 4. Exemplo de concorr�ncia utilizando lista ass�ncrona.

import java.util.ArrayList; import java.util.Collections; import java.util.List; public class ExemploAssincrono1 { private static int varCompartilhada = 0; private static final Integer QUANTIDADE = 10000; private static final List<Integer> VALORES = new ArrayList<>(); public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < QUANTIDADE; i++) { VALORES.add(++varCompartilhada); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < QUANTIDADE; i++) { VALORES.add(++varCompartilhada); } } }); Thread t3 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < QUANTIDADE; i++) { VALORES.add(++varCompartilhada); } } }); t1.start(); t2.start(); t3.start(); try { t1.join(); t2.join(); t3.join(); } catch (InterruptedException ex) { ex.printStackTrace(); } int soma = 0; for (Integer valor : VALORES) { soma += valor; } System.out.println("Soma: " + soma); } }

No entanto, ao executar este algoritmo � prov�vel que seja gerada a exce��o java.lang.ArrayIndexOutOfBoundsException, devido � concorr�ncia pela lista, visto que h� mais de uma thread tentando inserir dados nela. Como o �ponto fraco� desta estrutura de dados � seu mecanismo din�mico de tamanho vari�vel, a cada novo valor a ser inserido � preciso expandir a lista. Desta forma, a thread perde tempo para fazer esta opera��o, aumentando assim a possibilidade de ser pausada pelo escalonador. Quando isto acontece e alguma outra thread tenta realizar a mesma opera��o de add(), a exce��o � gerada. Com o intuito de solucionar esse problema, uma das op��es � adotar uma lista sincronizada, conforme o c�digo a seguir:

private static final List<Integer> VALORES = Collections.synchronizedList(new ArrayList<>());

Apesar de solucionar o problema anterior, ainda � poss�vel que a thread sofra interrup��o durante o incremento da vari�vel varCompartilhada e passe a gerar valores inconsistentes. Isto porque no processo atual de incremento da vari�vel, primeiramente deve ser pego o valor atual desta, som�-lo com 1 e ent�o obter o novo valor a ser armazenado.

Esse problema acontece porque nesse c�digo existem tr�s threads alterando o valor da mesma vari�vel (nesse caso, com o operador ++) e o escalonador, quando aloca uma thread ao processador, permite que ela execute seu c�digo por um determinado per�odo de tempo e depois a interrompe, possibilitando que outra thread ocupe seu lugar e opere sobre os mesmos dados. Assim, quando a thread anterior voltar a processar, trabalhar� com valores desatualizados.

Para aferir o resultado deste algoritmo, toda atualiza��o de valor da vari�vel varCompartilhada � adicionada a uma lista e ao final � realizada a soma de todos esses valores. Por causa das situa��es supracitadas, no entanto, o resultado gerado a cada execu��o pode ser diferente. Isto demonstra que o incremento de uma vari�vel ass�ncrona em threads �, sem d�vidas, um problema.

Nota: � preciso destacar que nem sempre ocorrer� esse problema, ou seja, nem sempre uma thread ser� interrompida durante o seu processamento. Para aumentar as chances desse problema acontecer, foi utilizado um intervalo de 10.000 repeti��es e tr�s threads. Com um n�mero baixo de itera��es, coincidentemente pode ser gerado o mesmo resultado em quase todas as execu��es.

O exemplo apresentado na Listagem 5 traz uma deriva��o do c�digo da Listagem 4. Neste caso, o List foi substitu�do por um Set, que suporta a inser��o de valores de modo ass�ncrono e ainda garante a unicidade dos valores inseridos. Assim, n�o mais teremos problemas com o ArrayList e poderemos dar sequ�ncia � demonstra��o do problema de concorr�ncia com a varCompartilhada.

Listagem 5. Exemplo de concorr�ncia utilizando HashSet.

import java.util.HashSet; import java.util.Set; public class ExemploAssincrono2 { private static int varCompartilhada = 0; private static final Integer QUANTIDADE = 10000; private static final Set<Integer> VALORES = new HashSet<>(); public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < QUANTIDADE; i++) { boolean novo = VALORES.add(++varCompartilhada); if (!novo) { System.out.println("J� existe: " + varCompartilhada); } } } }).start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < QUANTIDADE; i++) { boolean novo = VALORES.add(++varCompartilhada); if (!novo) { System.out.println("J� existe: " + varCompartilhada); } } } }).start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < QUANTIDADE; i++) { boolean novo = VALORES.add(++varCompartilhada); if (!novo) { System.out.println("J� existe: " + varCompartilhada); } } } }).start(); } }

Ao executar este algoritmo diversas vezes � poss�vel observar (vide Figura 6) que ele imprime no console alguns valores a serem inseridos que j� existem no Set, o que demonstra que as threads est�o incrementando a vari�vel, mas em algum momento geram o mesmo valor. Isso acontece por causa da concorr�ncia pela vari�vel varCompartilhada de maneira ass�ncrona, onde ao incrementar esta vari�vel, mais de uma thread acaba gerando o mesmo valor.

Figura 6. Resultado no console com a execu��o da Listagem 5.

Sincroniza��o de Threads

Caso n�o seja uma op��o substituir o ArrayList, uma alternativa para solucionar o problema obtido na Listagem 4 � sincronizar o objeto concorrido; neste caso, a lista (vide Listagem 6). Isso � poss�vel porque todo objeto Java possui um lock associado, que pode ser disputado por qualquer trecho de c�digo sincronizado e em qualquer thread.

Listagem 6. Exemplo de sincroniza��o de vari�vel com bloco de c�digo sincronizado.

import java.util.ArrayList; import java.util.List; public class ExemploBlocoSincronizado { //declara��o das vari�veis - vide Listagem 4 public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < QUANTIDADE; i++) { synchronized (VALORES) { VALORES.add(++varCompartilhada); } } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < QUANTIDADE; i++) { synchronized (VALORES) { VALORES.add(++varCompartilhada); } } } }); Thread t3 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < QUANTIDADE; i++) { synchronized (VALORES) { VALORES.add(++varCompartilhada); } } } }); //Idem Listagem 4... } }

Um bloco sincronizado previne que mais de uma thread consiga execut�-lo simultaneamente. Para isso, a thread que for utilizar esse bloco adquire o lock associado ao objeto sincronizado e as demais que tentarem acess�-lo entrar�o em estado de BLOCKED, at� que o objeto seja liberado. Na Figura 7 � poss�vel observar o ciclo de vida de uma thread, da sua cria��o � sua finaliza��o.

A seguir s�o descritos os poss�veis estados que elas podem assumir:

  • New: A thread dica neste estado ap�s criar sua inst�ncia e antes de invocar o m�todo start();
  • Runnable: Indica que ela est� executando na m�quina virtual Java;
  • Blocked: Ainda est� ativa, mas est� � espera por algum recurso que est� em uso por outra thread;
  • Waiting: Quando neste estado, ela est� � espera por tempo indeterminado pelo fato de outra thread ter executado uma determinada a��o. Isto ocorre quando se invoca o m�todo wait() ou join(), por exemplo;
  • Timed_Waiting: Neste estado a thread est� � espera de uma opera��o por um tempo pr�-determinado. Por exemplo, esta situa��o ocorre ao invocar m�todos como Thread.sleep(sleeptime), wait(timeout) ou join(timeout); e
  • Terminated: Este estado sinaliza que o m�todo run() finalizou.

Figura 7. Ciclo de vida de uma thread.

Nota: Ao sincronizar opera��es, prefira sempre o uso de m�todos sincronizados no lugar de blocos desse tipo. Isso porque os bytecodes gerados para um m�todo sincronizado s�o relativamente menores do que os gerados para um bloco sincronizado.

Outra forma de acessar um dado compartilhado entre threads � criando um m�todo sincronizado. Essa t�cnica � muito parecida com a anterior, mas ao inv�s de sincronizar o mesmo bloco de c�digo em cada thread, ele � transferido para um m�todo que cont�m a nota��o synchronized na assinatura. Assim, as threads ter�o que invoc�-lo para realizar a opera��o sobre o dado concorrente. Veja um exemplo na Listagem 7.

Listagem 7. Exemplo de m�todo sincronizado.

import java.util.ArrayList; import java.util.List; public class ExemploMetodoSincronizado { //Idem Listagem 4... public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < QUANTIDADE; i++) { incrementaEAdd(); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < QUANTIDADE; i++) { incrementaEAdd(); } } }); Thread t3 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < QUANTIDADE; i++) { incrementaEAdd(); } } }); //Idem Listagem 4 } private synchronized static void incrementaEAdd() { VALORES.add(++varCompartilhada); } }

Nota: O ato de adquirir bloqueios para sincronizar threads consome tempo, mesmo quando nenhuma precisa aguardar a libera��o do objeto sincronizado. Esse processo � uma faca de dois gumes: se por um lado ele resolve problemas de concorr�ncia, por outro serializa o processamento das threads sobre esse bloco; ou seja, as threads nunca estar�o processando esse c�digo simultaneamente, o que pode degradar o desempenho. Portanto, esse recurso deve ser usado com modera��o e somente onde for necess�rio.

Vari�veis at�micas

Quando � preciso utilizar tipos primitivos de forma concorrente uma boa op��o � adotar seu respectivo tipo at�mico, presente no pacote java.util.concurrent.atomic. Este tipo de objeto disponibiliza opera��es como incremento atrav�s de m�todos pr�prios e s�o executadas em baixo n�vel de hardware, de forma que a thread n�o ser� interrompida durante o processo. Deste modo n�o � necess�rio sincronizar o objeto, gerando um algoritmo sem bloqueios e muito mais r�pido. Veja o c�digo a seguir:

private static AtomicInteger varCompartilhada = new AtomicInteger(0);

Neste caso, ao inv�s de utilizar um Integer para armazenar o valor, foi instanciado um AtomicInteger. Com isso, pode-se trocar o varCompartilhada++ pela chamada varCompartilhada.incrementAndGet(), que realizar� uma fun��o semelhante de forma at�mica, o que garantir� que a thread n�o seja interrompida no meio do processo de incremento da vari�vel.

Nota: Em tipos at�micos, m�todos que n�o modificam seu valor s�o sincronizados.

Interface Callable

A interface Runnable � utilizada desde as primeiras vers�es da plataforma Java e como todos j� sabem, ela fornece um �nico m�todo � run() � que n�o aceita par�metros e n�o retorna valor, assim como n�o pode lan�ar qualquer tipo de exce��o. No entanto, e se precis�ssemos executar uma tarefa em paralelo e ao final obter um resultado como retorno? Para solucionar esse problema, voc� poderia criar um m�todo na classe que implementa Thread ou Runnable e esperar pela conclus�o da tarefa para acessar o resultado, assim como no cen�rio da Listagem 8.

Listagem 8. Exemplo de leitura de resultado em tarefa com Thread.

ThreadTarefa t = new ThreadTarefa(); t.start(); //inicia o trabalho da thread t.join(); //aguarda a thread finalizar String valor = t.getRetornoTarefa(); //acessa o resultado do processamento da tarefa.

Basicamente n�o h� nada de errado com esse c�digo, mas a partir do Java 5 este processo pode ser feito de forma diferente, gra�as � interface Callable. Deste modo, em vez de ter um m�todo run(), a interface Callable oferece um m�todo call(), que pode retornar um objeto qualquer, al�m da grande vantagem de poder capturar uma exce��o gerada pela tarefa da thread.

Para tirar proveito dos benef�cios de um objeto Callable, � altamente recomend�vel n�o utilizar um objeto Thread para execut�-lo, e sim alguma outra API, como:

  • ExecutorService: � uma API de alto n�vel para trabalhar diretamente com threads. Permite criar um pool de threads, reutiliz�-las e gerenci�-las; e
  • ExecutorCompletionService: � uma implementa��o da interface CompletionService que, associada a um ExecutorService, permite, atrav�s do m�todo take(), receber o resultado de cada tarefa conforme elas v�o finalizando, independente da ordem em que as tarefas foram criadas.

As implementa��es apresentadas nas Listagens 9 e 10 demonstram uma boa pr�tica no uso de Callables. Este c�digo cria tr�s tarefas que levam um determinado tempo para concluir e, ao terminar, retornam o nome da thread que a realizou. O c�digo da tarefa se encontra na classe ExemploCallable, que implementa a interface Callable, com retorno do tipo String. Com esta interface a tarefa que se deseja executar deve ser implementada no m�todo call() (vide Listagem 9), o qual � invocado ao executar o objeto Callable.

Listagem 9. Exemplo de classe implementando Callable.

import java.util.concurrent.Callable; public class ExemploCallable implements Callable<String> { private final long tempoDeEspera; public ExemploCallable(int time) { this.tempoDeEspera = time; } @Override public String call() throws Exception { Thread.sleep(tempoDeEspera); return Thread.currentThread().getName(); } }

Listagem 10. Exemplo de tarefas com retorno utilizando Callable.

package javamagazine.threads; import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExemploRetornoDeTarefa { public static void main(String[] args) { List<ExemploCallable> tarefas = Arrays.asList( new ExemploCallable(8000), new ExemploCallable(4000), new ExemploCallable(6000)); ExecutorService threadPool = Executors.newFixedThreadPool(3); ExecutorCompletionService<String> completionService = new ExecutorCompletionService<>(threadPool); //executa as tarefas for (ExemploCallable tarefa : tarefas) { completionService.submit(tarefa); } System.out.println("Tarefas iniciadas, aguardando conclus�o"); //aguarda e imprime o retorno de cada uma for (int i = 0; i < tarefas.size(); i++) { try { System.out.println(completionService.take().get()); } catch (InterruptedException | ExecutionException ex) { ex.printStackTrace(); } } threadPool.shutdown(); } }

O c�digo da Listagem 10 tem o objetivo de criar e executar tr�s tarefas armazenadas em uma lista. Para simular uma diferen�a no tempo de execu��o das threads, cada uma foi desenvolvida para aguardar um certo tempo em milissegundos, que lhe � fornecido no m�todo construtor. Antes de execut�-las, no entanto, note que � criado um pool de threads com um ExecutorService, o qual posteriormente � utilizado para criar um ExecutorCompletionService, que ser� encarregado de executar as tarefas e tamb�m nos ser� �til para receber o retorno de cada uma delas conforme forem concluindo.

Dito isso, uma a uma as tarefas s�o executadas atrav�s do m�todo submit() e, por fim, � utilizado o m�todo take(), para buscar a tarefa conclu�da, e o m�todo get(), que l� o retorno dela e o imprime no console (Figura 8).

Figura 8. Resultado da Listagem 10 no console.

Nota: Note, pelo resultado da Figura 8, que, por mais que a tarefa que tinha dura��o de oito segundos seja executada primeiro, seu resultado aparece por �ltimo. Esse resultado � obtido porque a leitura dos retornos de cada tarefa n�o tem rela��o com a ordem de execu��o, e sim com sua conclus�o, gra�as ao ExecutorCompletionService.

Cole��es concorrentes vs Cole��es sincronizadas

Um recurso bastante utilizado no desenvolvimento de software s�o as cole��es de dados. Na plataforma Java estas estruturas est�o dispon�veis em uma s�rie de implementa��es para os mais diversos fins. Como sabemos, n�o h� nenhum �mist�rio� em declar�-las, no entanto, como � comum nos depararmos com bugs ao acessar essas estruturas de maneira concorrente, vale dedicar um t�pico deste artigo para explorar suas peculiaridades.

Dentre as cole��es dispon�veis no Java, existem variados tipos de estruturas de dados, como, listas, pilhas e filas, e estas, por sua vez, ainda se subdividem quanto a forma de implementa��o, que compreende:

  • Cole��es sem suporte a threads: S�o as cole��es normalmente utilizadas. Encontradas no pacote java.util, como ArrayList, HashMap, HashSet, n�o devem ser utilizadas de forma concorrente, a menos que seja feito um sincronismo externo sobre a cole��o;
  • Cole��es sincronizadas: Podem ser criadas a partir de m�todos est�ticos dispon�veis na classe java.util.Collections, por exemplo: java.util.Collections.synchronizedList(objetoLista). Como estes m�todos retornam uma cole��o sincronizada, isto significa que seu acesso para modifica��es ocorre de forma serializada, ou seja, somente uma thread por vez pode acess�-la; e
  • Cole��es concorrentes: N�o necessitam de nenhum sincronismo adicional, como sincronizar seu objeto ou algum m�todo, pois possuem um sofisticado suporte para concorr�ncia. Estas cole��es, livres de problemas advindos da concorr�ncia entre threads, podem ser encontradas no pacote java.util.concurrent.

Sabendo disso, preferencialmente, opte por utilizar cole��es concorrentes, ao inv�s das sincronizadas, pois as cole��es concorrentes possuem maior escalabilidade e suportam modifica��es simult�neas de diversas threads sem precisar estabelecer um bloqueio. J� as cole��es sincronizadas t�m sua performance degradada devido ao bloqueio que precisam estabelecer quando uma thread as acessa. Logo, isso tamb�m significa que somente uma thread por vez pode modific�-las.

Um detalhe que costuma passar despercebido nas entrelinhas da programa��o concorrente � que n�o existe a garantia de execu��o paralela ou de que cada thread vai executar em um n�cleo diferente. Criar threads apenas sugere � JVM que aquilo seja paralelizado. Por exemplo, voc� pode ter um processador de quatro n�cleos e criar um aplicativo com quatro threads que processem exaustivamente, mas isso n�o lhe garante que cada uma das quatro threads ser�o executadas por um n�cleo diferente, t�o pouco consumir�o 100% de processamento. Portanto, n�o basta criar threads pensando que isto � a solu��o dos seus problemas. Neste caso, ao criar threads em demasia estar-se-ia degradando a performance, j� que a JVM gastaria muito tempo com o escalonamento delas, se comparado ao tempo total de processamento utilizado pelas threads.

Primeiramente, a aplica��o deve ser inteligente o bastante para criar o n�mero ideal de threads, ou seja, deve ser levada em considera��o a quantidade de processadores/n�cleos dispon�veis no sistema. Criar um n�mero de threads menor do que o n�mero de n�cleos dispon�veis gera desperd�cio. Por outro lado, gerar um n�mero excessivamente maior de threads, causar� outro problema. Ser� perdido mais tempo com o escalonamento das threads do que com as pr�prias tarefas que elas precisam executar, e assim, por mais que se esteja consumindo 100% da CPU, n�o se tem o desempenho m�ximo que se pode atingir.

Para amenizar este problema, um recurso muito �til da plataforma Java pode ser verificado no c�digo apresentado a seguir, que permite ler a quantidade de n�cleos dispon�veis. A partir disso, podemos calcular o n�mero ideal de threads necess�rias para atingir os 100% de processamento sem desperd�cios, quando temos uma aplica��o que precisa realizar um c�lculo exaustivo:

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

FrameworkFork/Join

O frameworkFork/Join, introduzido na vers�o 7 da plataforma Java, � uma implementa��o da interface ExecutorService que auxilia o desenvolvedor a tirar proveito das arquiteturas multicore. Esta API foi projetada para as tarefas que podem ser quebradas em pequenas partes recursivamente, com o objetivo de usar todo o poder de processamento dispon�vel para melhorar o desempenho da aplica��o.

O exemplo apresentado nas Listagens 11 e 12 demonstra um cen�rio onde o objetivo � buscar, recursivamente em um sistema de arquivos, os arquivos com determinada extens�o. Ao iniciar, a tarefa recebe um diret�rio base onde o algoritmo come�a as buscas. O conte�do do diret�rio � ent�o analisado e caso haja outra pasta dentro desta, � criada outra tarefa para analisar aquele diret�rio, e assim recursivamente o algoritmo realiza a busca pelos arquivos e retorna os resultados � tarefa pai.

Tecnicamente, para realizar este processo foi implementada uma classe que estende RecursiveTask e recebe um List de String, o qual � utilizado para informar o tipo de retorno da tarefa (vide Listagem 11). Ao criar a tarefa, ou seja, uma inst�ncia da classe ProcessadorDePastas, � necess�rio informar por par�metros o diret�rio base onde se iniciar� a busca e a extens�o de arquivo pela qual se dar� a busca.

Quando se estende a classe RecursiveTask, deve ser implementado o m�todo compute(), que � respons�vel por desempenhar a tarefa desejada, assim como devemos codificar o m�todo run(), quando se implementa a interface Runnable. � neste m�todo que est� especificada a busca pelos arquivos. Nele, o ponto mais importante pode ser verificado na recursividade, local que cria as tarefas paralelas com a chamada ao m�todo fork() para cada pasta localizada dentro da pasta na qual se est� pesquisando. Ao final, cada subtarefa retorna os dados de sua busca � tarefa que a criou, e esta, por sua vez, adiciona estes dados na lista �tarefas�. Este � o processo de desempilhar a recurs�o, que � realizado at� chegar � primeira tarefa criada na classe ForkJoinMain, momento este em que os dados s�o retornados para a lista resultados pelo m�todo join() (vide Listagem 12).

Listagem 11. Exemplo de tarefa Fork/Join.

import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.concurrent.RecursiveTask; public class ProcessadorDePastas extends RecursiveTask<List<String>> { private final String diretorio; private final String extensao; public ProcessadorDePastas(String diretorio, String extension) { this.diretorio = diretorio; this.extensao = extension; } @Override protected List<String> compute() { List<String> lista = new ArrayList<>(); List<ProcessadorDePastas> tarefas = new ArrayList<>(); File arquivo = new File(diretorio); File conteudo[] = arquivo.listFiles(); if (conteudo != null) { for (int i = 0; i < conteudo.length; i++) { if (conteudo[i].isDirectory()) { ProcessadorDePastas tarefa = new ProcessadorDePastas(conteudo[i].getAbsolutePath(), extensao); tarefa.fork(); tarefas.add(tarefa); } else if (verificaArquivo(conteudo[i].getName())) { lista.add(conteudo[i].getAbsolutePath()); } } } if (tarefas.size() > 50) { System.out.printf("%s: %d tarefas executando.\n", arquivo.getAbsolutePath(), tarefas.size()); } addResultadosDaTarefa(lista, tarefas); return lista; } private void addResultadosDaTarefa(List<String> lista, List<ProcessadorDePastas> tarefas) { for (ProcessadorDePastas item : tarefas) { lista.addAll(item.join()); } } private boolean verificaArquivo(String nome) { return nome.endsWith(extensao); } }

Na Listagem 12 temos o c�digo respons�vel por iniciar a tarefa principal, ler e exibir os resultados. Para tal, foram criadas tr�s tarefas base que far�o as buscas em tr�s pastas distintas, e a fim de execut�-las, foi instanciado um pool de threads com um ForkJoinPool. Este tipo de pool gerencia de forma mais eficiente o trabalho das threads, pois utiliza uma t�cnica chamada de �roubo de tarefa� para executar as tarefas em espera. Nesta abordagem cada thread possui uma fila de tarefas em espera e no momento em que uma thread n�o tiver mais nada em sua fila, poder� �roubar� o trabalho de outra, possibilitando mais uma melhoria na performance.

Listagem 12. Exemplo de uso da tarefa Fork/Join.

import java.util.List; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; public class ForkJoinMain { public static void main(String[] args) { ProcessadorDePastas sistema = new ProcessadorDePastas("C:/Windows", ".exe"); ProcessadorDePastas aplicativos = new ProcessadorDePastas("C:/Program Files", ".exe"); ProcessadorDePastas documentos = new ProcessadorDePastas("C:/users", ".doc"); ForkJoinPool pool = new ForkJoinPool(); pool.execute(sistema); pool.execute(aplicativos); pool.execute(documentos); do { System.out.printf("----------------------------------------\n"); System.out.printf("-> Paralelismo: %d\n", pool.getParallelism()); System.out.printf("-> Threads Ativas: %d\n", pool.getActiveThreadCount()); System.out.printf("-> Tarefas: %d\n", pool.getQueuedTaskCount()); System.out.printf("-> Roubos: %d\n", pool.getStealCount()); System.out.printf("----------------------------------------\n"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } while ((!sistema.isDone()) || (!aplicativos.isDone()) || (!documentos.isDone())); pool.shutdown(); List<String> resultados; resultados = sistema.join(); System.out.printf("Sistema: %d aplicativos encontrados.\n", resultados.size()); resultados = aplicativos.join(); System.out.printf("Aplicativos: %d encontrados.\n", resultados.size()); resultados = documentos.join(); System.out.printf("Documentos: %d encontrados.\n", resultados.size()); } }

Por fim, saiba que enquanto o aplicativo processa � poss�vel extrair algumas informa��es �teis, a fim de monitorar o trabalho do framework e do pool de threads. Estes dados podem ser obtidos com o pr�prio objeto do pool, atrav�s dos seguintes m�todos:

  • getParallelism(): Retorna o n�vel do paralelismo. Por default e por recomenda��o, � a quantidade de n�cleos do processador;
  • getActiveThreadCount(): Retorna a quantidade de threads ativas;
  • getQueuedTaskCount(): Retorna a quantidade total de tarefas na fila de espera; e
  • getStealCount(): Retorna a quantidade de roubos que ocorreram. Um roubo ocorre quando uma thread fica sem trabalho. Ent�o ela rouba tarefas da fila de espera de outra thread.

Java 8 � Lambdas e Streams

A vers�o 8 da plataforma Java tem como uma das suas principais caracter�sticas o suporte a express�es Lambda, recurso que foi projetado com o intuito de facilitar a programa��o funcional e reduzir o tempo de desenvolvimento. Isso pode ser exemplificado criando um objeto Thread, como exp�e o c�digo da Listagem 13, onde � poss�vel notar que a cria��o do objeto Runnable se torna impl�cita, reduzindo de cinco para duas a quantidade de linhas necess�rias para a cria��o de uma thread.

Listagem 13. Criando uma thread com express�es lambda.

new Thread(() -> { //C�digo da tarefa a ser executada }).start();

Ainda no Java 8, uma nova abstra��o, chamada Stream, foi desenvolvida. Esta permite processar dados de forma declarativa, assim como possibilita a execu��o de tarefas utilizando v�rios n�cleos sem que seja necess�rio implementar uma linha de c�digo multithreading, atrav�s da fun��o parallelStream. Quando um stream � executado em paralelo, a JVM o particiona em v�rios substreams, os quais s�o iterados individualmente por threads e, por fim, seus resultados s�o combinados (veja Listagem 14).

Al�m de ser extremamente simples e funcional, em poucas linhas � poss�vel extrair v�rias informa��es de uma lista num�rica, como valor m�ximo, m�nimo, soma total e m�dia, sem ter que se preocupar em desenvolver estas fun��es. E mesmo que sua lista n�o seja num�rica, ainda assim se tornou mais f�cil transformar ou extrair informa��es por meio das express�es lambda.

Listagem 14. Exemplo utilizando ParallelStream.

import java.util.ArrayList; import java.util.List; import java.util.LongSummaryStatistics; import java.util.Random; public class ExemploParallelStream { public static void main(String[] args) { List<Long> numeros = new ArrayList<>(); Random random = new Random(); for (int i = 0; i < 10000000; i++) { numeros.add(random.nextLong()); } LongSummaryStatistics stats = numeros.parallelStream().mapToLong((x) -> x ).summaryStatistics(); System.out.println("Maior n�mero na lista: " + stats.getMax()); System.out.println("Menor n�mero na lista: " + stats.getMin()); System.out.println("Soma de todos os n�meros: " + stats.getSum()); System.out.println("M�dia de todos os n�meros: " + stats.getAverage()); } }

O Java foi uma das primeiras plataformas a fornecer suporte a multithreading no n�vel de linguagem e agora � uma das primeiras a padronizar utilit�rios e APIs de alto n�vel para lidar com threads, como a introdu��o do framework Fork/Join na vers�o 7, e a API de streams e o suporte a express�es lambda na vers�o 8.

Atualmente, qualquer computador ou smartphone tem mais de um n�cleo de processamento e a cada novo lan�amento esta quantidade s� aumenta, assim como a import�ncia do software ser desenvolvido em multithreading. Atendendo a esse cen�rio, o Java fornece uma base s�lida para a cria��o de uma ampla variedade de solu��es paralelas.

Para finalizar, note que � poss�vel alcan�ar bons resultados com as t�cnicas aqui demonstradas. Contudo, sempre utilize a programa��o concorrente com bastante aten��o, pois ao manipular dados compartilhados entre threads poder� cair em alguns cen�rios de depura��o bem dif�ceis.

Tecnologias:
  • Java
Voltar

Confira outros conte�dos:

Plano PRO

  • Acesso completo
  • Projetos reais
  • Professores online
  • Exerc�cios gamificados
  • Certificado de autoridade

Por Rodrigo Em 2016

Receba nossas novidades

O QUE É paralelismo de tarefas?

Paralelismo de tarefa é a forma mais simples de programação paralela, onde as aplicações estão divididas em tarefas exclusivas que são independentes umas das outras e podem ser executadas em processadores diferentes.

Quais as vantagens do paralelismo?

Em grandes empresas e indústrias, o paralelismo contribui para evitar prejuízos que podem ocorrer durante a manutenção do gerador, já que com mais de um equipamento em funcionamento, não há problemas com interrupção do fornecimento de energia, e automaticamente, da produção.

São as operações usadas em CUDA exceto?

Essa API será a: MPI (Message Passing Interface) Cilk++ POSIX CUDA (Compute Unified Device Architecture) OpenMP Respondido em 23/09/2021 16:57:16 Explicação: A resposta certa é: MPI (Message Passing Interface) 9a Questão Acerto: 1,0 / 1,0 São as operações usadas em CUDA, exceto: Transferência de dados.

Qual é a maior vantagem de usar paralelismo?

A maior vantagem do paralelismo OU é o fato de que ele vem de forma natural e sem muito overhead.

Toplist

Última postagem

Tag