9 erros a evitar ao programar em Java
Que erros devem ser evitados ao programar em Java? No artigo seguinte, respondemos a esta pergunta.
Leia a primeira parte da nossa série de blogues dedicada à concorrência em Java. No artigo a seguir, veremos mais de perto as diferenças entre thread e processos, pools de threads, executores e muito mais!
Em geral, a abordagem de programação convencional é sequencial. Tudo num programa acontece um passo de cada vez.
Mas, na verdade, o paralelo é a forma como o mundo inteiro funciona - é a capacidade de executar mais do que uma tarefa em simultâneo.
Para debater temas avançados como concorrência em Java ou multithreading, temos de chegar a acordo sobre algumas definições comuns para termos a certeza de que estamos na mesma página.
Comecemos pelo básico. No mundo não-sequencial, temos dois tipos de representações de concorrência: processos e
threads. Um processo é uma instância do programa em execução. Normalmente, está isolado de outros processos.
O sistema operativo é responsável pela atribuição de recursos a cada processo. Além disso, actua como um condutor que
programa e controla-os.
A thread é uma espécie de processo, mas num nível inferior, pelo que também é conhecida como light thread. Várias threads podem ser executadas numa
processo. Aqui o programa actua como um agendador e um controlador de threads. Desta forma, os programas individuais parecem fazer
várias tarefas ao mesmo tempo.
A diferença fundamental entre threads e processos é o nível de isolamento. O processo tem seu próprio conjunto de
enquanto a thread partilha dados com outras threads. Pode parecer uma abordagem propensa a erros e, de facto, é. Para
Não nos vamos debruçar sobre isso, uma vez que ultrapassa o âmbito deste artigo.
Processos, threads - ok... Mas o que é exatamente a concorrência? Concorrência significa que é possível executar várias tarefas ao mesmo tempo
tempo. Não significa que essas tarefas tenham de ser executadas em simultâneo - é isso que é o paralelismo. Concurrenc em Javay também não
requer que tenha várias CPUs ou mesmo vários núcleos. Pode ser conseguido num ambiente de núcleo único, tirando partido de
mudança de contexto.
Um termo relacionado com a concorrência é o multithreading. Esta é uma caraterística dos programas que lhes permite executar várias tarefas ao mesmo tempo. Nem todos os programas utilizam esta abordagem, mas os que o fazem podem ser designados por multithreaded.
Estamos quase prontos para começar, só mais uma definição. Assincronia significa que um programa executa operações sem bloqueio.
Inicia uma tarefa e depois continua com outras coisas enquanto espera pela resposta. Quando obtém a resposta, pode reagir a ele.
Por defeito, cada Aplicação Java é executado num processo. Nesse processo, existe um thread relacionado com o principal() método de
uma aplicação. No entanto, como já foi referido, é possível tirar partido dos mecanismos de múltiplas threads numa
programa.
Linha é um Java na qual a magia acontece. Esta é a representação do objeto da thread mencionada anteriormente. Para
criar o seu próprio thread, pode estender o Linha classe. No entanto, esta não é uma abordagem recomendada. Fios deve ser utilizado como um mecanismo para executar a tarefa. As tarefas são partes de código que queremos executar num modo concorrente. Podemos defini-los utilizando a função Executável interface.
Mas chega de teoria, vamos pôr o nosso código onde está a nossa boca.
Suponhamos que temos um par de matrizes de números. Para cada matriz, queremos saber a soma dos números numa matriz. Vamos
É suposto que existam muitas destas matrizes e que cada uma delas seja relativamente grande. Nessas condições, queremos utilizar a concorrência e somar cada matriz como uma tarefa separada.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Runnable task1 = () -> {
int soma = Arrays.stream(a1).soma();
System.out.println("1. A soma é: " + soma);
};
Runnable task2 = () -> {
int sum = Arrays.stream(a2).sum();
System.out.println("2. A soma é: " + soma);
};
Runnable task3 = () -> {
int sum = Arrays.stream(a3).sum();
System.out.println("3. A soma é: " + soma);
};
nova Thread(task1).start();
nova Thread(tarefa2).start();
new Thread(task3).start();
Como pode ver no código acima Executável é uma interface funcional. Contém um único método abstrato executar()
sem argumentos. O Executável deve ser implementada por qualquer classe cujas instâncias se destinem a ser
executada por um thread.
Depois de definir uma tarefa, pode criar um segmento para a executar. Isto pode ser feito através de novo Thread() construtor que
toma Executável como seu argumento.
O último passo é iniciar() um segmento recentemente criado. Na API existem também executar() métodos em Executável e emLinha. No entanto, essa não é uma forma de aproveitar a concorrência em Java. Uma chamada direta a cada um destes métodos resulta em
executando a tarefa no mesmo thread que o principal() método é executado.
Quando existem muitas tarefas, criar um thread separado para cada uma não é uma boa ideia. Criar uma Linha é um
operação pesada e é muito melhor reutilizar os fios existentes do que criar novos.
Quando um programa cria muitos threads de curta duração, é melhor usar um pool de threads. O pool de threads contém um número de
tópicos prontos para serem executados mas atualmente não activos. Dando um Executável para o pool faz com que uma das threads chame a funçãoexecutar() método de um determinado Executável. Após a conclusão de uma tarefa, o segmento continua a existir e está num estado inativo.
Ok, já percebeu - prefira o pool de threads em vez da criação manual. Mas como é que se pode utilizar os pools de threads? O Executores
tem uma série de métodos estáticos de fábrica para construir pools de threads. Por exemplo newCachedThredPool() cria
uma piscina na qual são criadas novas threads conforme necessário e as threads inactivas são mantidas durante 60 segundos. Em contrapartida,newFixedThreadPool() contém um conjunto fixo de threads, no qual as threads inactivas são mantidas indefinidamente.
Vejamos como pode funcionar no nosso exemplo. Agora não precisamos de criar threads manualmente. Em vez disso, temos de criarExecutorService que fornece um conjunto de threads. Em seguida, podemos atribuir-lhe tarefas. O último passo é fechar a thread
para evitar fugas de memória. O resto do código anterior permanece o mesmo.
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.shutdown();
Executável parece ser uma forma elegante de criar tarefas concorrentes, mas tem uma grande falha. Ele não pode retornar nenhum
valor. Além disso, não podemos determinar se uma tarefa está concluída ou não. Também não sabemos se foi concluída
normal ou excecionalmente. A solução para estes males é Acessível.
Acessível é semelhante a Executável de certa forma, também envolve tarefas assíncronas. A principal diferença é que ele é capaz de
devolver um valor. O valor de retorno pode ser de qualquer tipo (não primitivo) como o Acessível é um tipo parametrizado.Acessível é uma interface funcional que tem chamar() que pode lançar um método Exceção.
Agora vamos ver como podemos aproveitar Acessível no nosso problema da matriz.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Callable task1 = () -> Arrays.stream(a1).sum();
Callable task2 = () -> Arrays.stream(a2).sum();
Callable task3 = () -> Arrays.stream(a3).sum();
ExecutorService executor = Executors.newCachedThreadPool();
Future future1 = executor.submit(task1);
Future future2 = executor.submit(task2);
Future future3 = executor.submit(task3);
System.out.println("1. A soma é: " + future1.get());
System.out.println("2. A soma é: " + future2.get());
System.out.println("3. A soma é: " + future3.get());
executor.shutdown();
Ok, podemos ver como Acessível é criado e depois apresentado ao ExecutorService. Mas o que raio é Futuro?Futuro actua como uma ponte entre threads. A soma de cada matriz é produzida numa thread separada e precisamos de uma forma de
obter esses resultados de volta para principal().
Para obter o resultado de Futuro precisamos de chamar get() método. Aqui pode acontecer uma de duas coisas. Primeiro, o
resultado do cálculo efectuado por Acessível estiver disponível. Então, recebemo-lo imediatamente. Em segundo lugar, o resultado não é
já está pronto. Neste caso get() irá bloquear até que o resultado esteja disponível.
O problema com Futuro é o facto de funcionar segundo o "paradigma push". Ao utilizar Futuro tem de ser como um chefe que
pergunta constantemente: "A sua tarefa está concluída? Está pronta?" até obter um resultado. Atuar sob pressão constante é
caro. Uma abordagem muito melhor seria encomendar Futuro o que fazer quando estiver pronto para a sua tarefa. Infelizmente,Futuro não pode fazer isso, mas ComputableFuture pode.
ComputableFuture funciona no 'paradigma pull'. Podemos dizer-lhe o que fazer com o resultado quando tiver concluído as suas tarefas. O
é um exemplo de uma abordagem assíncrona.
ComputableFuture funciona perfeitamente com Executável mas não com Acessível. Em vez disso, é possível fornecer uma tarefa aoComputableFuture sob a forma de Fornecedor.
Vejamos como o que foi dito acima se relaciona com o nosso problema.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
CompletableFuture.supplyAsync(() -> Arrays.stream(a1).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a2).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a3).sum())
.thenAccept(System.out::println);
A primeira coisa que nos chama a atenção é o facto de esta solução ser muito mais curta. Para além disso, também tem um aspeto limpo e arrumado.
Tarefa para CompletableFuture pode ser fornecido por supplyAsync() método que recebe Fornecedor ou por runAsync() que
toma Executável. Um retorno de chamada - um pedaço de código que deve ser executado na conclusão da tarefa - é definido por thenAccept()
método.
Java fornece muitas abordagens diferentes para a concorrência. Neste artigo, mal tocámos no assunto.
No entanto, abordámos as noções básicas de Linha, Executável, Acessívele CallableFuture o que coloca uma boa questão
para uma investigação mais aprofundada do tema.
