Participe da Maratona Behind the Code! A competição de programação que mais te desafia! Inscreva-se aqui

Cinco coisas que você não sabia sobre… java.util.concurrent, Parte 2

As coleções simultâneas facilitam a programação simultânea fornecendo estruturas de dados thread-safe bem ajustadas. No entanto, em alguns casos os desenvolvedores precisam ir além e pensar sobre regulação e/ou regulação da execução de encadeamento. Visto que a intenção principal do java.util.concurrent é simplificar a programação multiencadeada, é esperado que o pacote inclua utilitários de sincronização — e ele inclui.

Este artigo, uma continuação da Parte 1, introduz diversas construções de sincronização com um nível mais alto do que as linguagens principais primitivas (monitores), mas não tão alto de forma que são integradas em uma classe de coleção. O uso desses bloqueios e barreiras é muito simples quando se conhece seus objetivos.

1. Semáforo

Em alguns sistemas corporativos, não é incomum os desenvolvedores precisarem regular o número de solicitações abertas (encadeamentos/ações) com relação a um recurso particular — na verdade, a regulação pode ocasionalmente melhorar o rendimento de um sistema reduzindo a quantidade de contenção com relação a esse recurso específico. Mesmo que seja seguramente possível tentar escrever o código regulador a mão, é mais fácil usar a classe de semáforo, que cuida da regulação para você, conforme mostrado na Listagem 1:






Listagem 1. Use o semáforo para regular

import java.util.*;import java.util.concurrent.*;

public class SemApp
{
    public static void main(String[] args)
    {
        Runnable limitedCall = new Runnable() {
            final Random rand = new Random();
            final Semaphore available = new Semaphore(3);
            int count = 0;
            public void run()
            {
                int time = rand.nextInt(15);
                int num = count++;

                try
                {
                    available.acquire();

                    System.out.println("Executing " +
                        "long-running action for " +
                        time + " seconds... #" + num);

                    Thread.sleep(time * 1000);

                    System.out.println("Done with #" +
                        num + "!");

                    available.release();
                }
                catch (InterruptedException intEx)
                {
                    intEx.printStackTrace();
                }
            }
        };

        for (int i=0; i<10; i++)
            new Thread(limitedCall).start();
    }
}

Apesar dos 10 encadeamentos desta amostra estarem em execução (o que pode ser verificado executando o jstack com relação ao processo Java SemApp em execução), apenas três estão ativos. Os outros sete estão suspensos no compartimento até uma das contagens do semáforo ser liberada. (Na verdade, a classe de semáforo oferece suporte para a obtenção e liberação de mais de um permit por vez, mas isso não faria sentido neste cenário.)

2. CountDownLatch

Se o semáforo é a classe de simultaneidade projetada para permitir a entrada de encadeamentos, um de cada vez (lembrando os seguranças das casas noturnas populares), o CountDownLatch seria o portão de largada de uma corrida de cavalos. Essa classe mantém todos os encadeamentos no compartimento até o cumprimento de uma condição específica, quando todos os encadeamentos são liberados juntos.

Listagem 2. CountDownLatch: Que comecem as corridas!

import java.util.*;
import java.util.concurrent.*;

class Race
{
    private Random rand = new Random();

    private int distance = rand.nextInt(250);
    private CountDownLatch start;
    private CountDownLatch finish;

    private List<String> horses = new ArrayList<String>();

    public Race(String... names)
    {
        this.horses.addAll(Arrays.asList(names));
    }

    public void run()
        throws InterruptedException
    {
        System.out.println("And the horses are stepping up to the gate...");
        final CountDownLatch start = new CountDownLatch(1);
        final CountDownLatch finish = new CountDownLatch(horses.size());
        final List<String> places =
            Collections.synchronizedList(new ArrayList<String>());

        for (final String h : horses)
        {
            new Thread(new Runnable() {
                public void run() {
                    try
                    {
                        System.out.println(h +
                            " stepping up to the gate...");
                        start.await();

                        int traveled = 0;
                        while (traveled < distance)
                        {
                            // In a 0-2 second period of time....
                            Thread.sleep(rand.nextInt(3) * 1000);

                            // ... a horse travels 0-14 lengths
                            traveled += rand.nextInt(15);
                            System.out.println(h +
                                " advanced to " + traveled + "!");
                        }
                        finish.countDown();
                        System.out.println(h +
                            " crossed the finish!");
                        places.add(h);
                    }
                    catch (InterruptedException intEx)
                    {
                        System.out.println("ABORTING RACE!!!");
                        intEx.printStackTrace();
                    }
                }
            }).start();
        }

        System.out.println("And... they're off!");
        start.countDown();

        finish.await();
        System.out.println("And we have our winners!");
        System.out.println(places.get(0) + " took the gold...");
        System.out.println(places.get(1) + " got the silver...");
        System.out.println("and " + places.get(2) + " took home the bronze.");
    }
}

public class CDLApp
{
    public static void main(String[] args)
        throws InterruptedException, java.io.IOException
    {
        System.out.println("Prepping...");

        Race r = new Race(
            "Beverly Takes a Bath",
            "RockerHorse",
            "Phineas",
            "Ferb",
            "Tin Cup",
            "I'm Faster Than a Monkey",
            "Glue Factory Reject"
            );

        System.out.println("It's a race of " + r.getDistance() + " lengths");

        System.out.println("Press Enter to run the race....");
        System.in.read();

        r.run();
    }
}

Observe na Listagem 2. CountDownLatch: Que comecem as corridas! que o CountDownLatch tem duas funções: Primeiro ele libera todos os encadeamentos simultaneamente, simulando o início de uma corrida; mas posteriormente uma trava diferente simula o fim da corrida, basicamente para que o encadeamento “principal” possa imprimir os resultados. Para uma corrida com mais comentários, é possível incluir os CountDownLatch es em pontos específicos e na metade do caminho da corrida, à medida que os cavalos cruzam os valores de um quarto, da metade e de três quartos da distância.

3. Executor

Os exemplos na Listagem 1. Use o semáforo para regular e na Listagem 2. CountDownLatch: Que comecem as corridas! possuem uma falha bastante frustrante, pois forçam a criação de objetos Thread diretamente. Esta é uma fórmula para problemas, pois em algumas JVMs, a criação de um encadeamento é uma operação complicada, sendo muito mais fácil a reutilização dos encadeamentos existentes do que a criação de novos encadeamentos. No entanto, em outras JVMs, ocorre exatamente o inverso: os encadeamentos são muito leves e é muito mais fácil criar um novo encadeamento quando necessário. É óbvio que Murphy estará presente (ele sempre está), sendo assim, independente da abordagem usada, será sempre a abordagem errada para a plataforma que está sendo implementada.

O Grupo de Especialistas em JSR-166 (consulte a seção Recursos) antecipou essa situação de alguma forma. Ao invés dos desenvolvedores Java criarem encadeamentos diretamente, eles introduziram a interface Executor, uma abstração para a criação de novos encadeamentos. Conforme mostrado na Listagem 3, o Executor permite a criação de encadeamentos sem a necessidade de criação de novos objetos Thread:

Listagem 3. Executor

Executor exec = getAnExecutorFromSomeplace();
exec.execute(new Runnable() { ... });

A principal desvantagem do uso do Executor é a mesma encontrada com todas as factories: o factory tem que vir de algum lugar. Infelizmente, diferente do CLR, a JVM não vem com um conjunto de encadeamentos abrangente de VM padrão.

A classe Executoratua_ com um local comum de obtenção de instâncias de implementação do Executor, mas ela possui somente novos métodos (para criar um novo conjunto de encadeamentos, por exemplo); ela não possui instâncias pré-criadas. Então você estará por sua conta se quiser criar e usar instâncias do Executor no seu código. (Ou, em alguns casos, será possível usar uma instância fornecida pelo container/plataforma escolhido.)

ExecutorService, ao seu dispor

Por mais que seja útil não ter que se preocupar com a origem dos encadeamentos, a interface Executor carece de algumas funcionalidades esperadas pelo desenvolvedor Java, como a capacidade de acionar um encadeamento projetado para produzir um resultado e esperar, sem bloqueios, que esse resultado fique disponível. (Isso é uma necessidade muito comum em aplicativos de desktop, nos quais o usuário executa uma operação de UI que requer o acesso a um banco de dados e deseja cancelar a operação antes da sua conclusão se ela demorar muito.)

Para isso, os especialistas em JSR-166 criaram uma abstração muito mais útil, a interface do ExecutorService, que modela o factory iniciador do encadeamento como um serviço que pode ser controlado coletivamente. Por exemplo: ao invés de chamar o execute() uma vez para cada tarefa, o ExecutorService pode obter uma coleção de tarefas e retornar uma Lista de Futuros representando os resultados futuros de cada uma das tarefas.

4. ScheduledExecutorServices

Por melhor que a interface ExecutorService seja, certas tarefas precisam ser executadas de maneira planejada, como a execução de uma determinada tarefa em intervalos determinados ou em um horário específico. Essa é a atribuição do ScheduledExecutorService, que estende o ExecutorService.

Se o seu objetivo for criar um comando de “pulsação” que executava um “ping” a cada cinco segundos, o ScheduledExecutorService tornará essa tarefa simples, conforme visto na Listagem 4:

Listagem 4. ‘Pings’ do ScheduledExecutorService planejados

import java.util.concurrent.*;

public class Ping
{
    public static void main(String[] args)
    {
        ScheduledExecutorService ses =
            Executors.newScheduledThreadPool(1);
        Runnable pinger = new Runnable() {
            public void run() {
                System.out.println("PING!");
            }
        };
        ses.scheduleAtFixedRate(pinger, 5, 5, TimeUnit.SECONDS);
    }
}

Que tal? Sem confusão com os encadeamentos, sem confusão com o que fazer se o usuário quiser cancelar a pulsação, sem a marcação explícita de encadeamentos no primeiro plano ou no segundo plano; simplesmente deixe todos esses detalhes de planejamento para o ScheduledExecutorService.

Casualmente, seu um usuário quiser cancelar a pulsação, o retorno da chamada scheduleAtFixedRate seria uma instância do ScheduledFuture, que não somente envolve o resultado, se houver algum, mas também possui um método cancel para encerrar a operação planejada.

5. Métodos de expiração

A capacidade de colocação de um tempo de espera concreto nas operações de bloqueio (evitando dessa forma os conflitos) é um dos excelentes avanços da biblioteca do java.util.concurrent em relação aos seus parentes antigos de simultaneidade, como os monitores para bloqueio.

Esses métodos na maioria das vezes são sobrecarregados com um par int / TimeUnit, indicando quanto tempo o método deve esperar antes de serem liberados e retornarem o controle para o programa. Isso requer mais trabalho por parte do desenvolvedor — como recuperar se o bloqueio não for obtido? — mas na maioria das vezes os resultados são mais corretos: menos conflitos e mais códigos de segurança de produção. (Para obter mais informações sobre a composição de códigos prontos para produção, consulte o artigo Release It! de Michael Nygard na seção Recursos.)

Conclusão

O pacote java.util.concurrent contém muitos utilitários inteligentes que vão além das coleções, principalmente nos pacotes .locks e .atomic. Procure e você também localizará estruturas de controle úteis como o CyclicBarrier, entre outras.

Como ocorre em muitos aspectos da plataforma Java, não é necessário procurar muito para localizar códigos de infraestrutura que podem ser muito úteis. Sempre que estiver compondo códigos multiencadeados, lembre-se dos utilitários discutidos neste e no artigo anterior.

Da próxima vez, iremos explorar um novo tópico: cinco coisas que você não sabia sobre Jars.

Download

5things5-src.zip: Sample code for this article

Aviso

O conteúdo aqui presente foi traduzido da página IBM Developer US. Caso haja qualquer divergência de texto e/ou versões, consulte o conteúdo original.