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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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.
Em relação aos conceitos de paralelismo de tarefas, considere as afirmações a seguir.

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).

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

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

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

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.