Пять вещей, которые вы не знали о ... пакете java.util.concurrent. Часть 2

Параллельное программирование означает работу с большей смекалкой, а не с большими усилиями

Помимо дружелюбных к параллелизму коллекций, в пакете java.util.concurrent имеется еще один встроенный компонент, который может помочь регулировать и выполнять потоки в многопоточных приложениях. Тед Ньювард представляет еще пять вещей из пакета java.util.concurrent, которые обязательно должен знать разработчик на Java™.

Тед Ньювард, Глава, Neward & Associates

Тед Ньювард - глава Neward & Associates, где он консультирует, руководит, обучает и внедряет Java, .NET, XML Services и другие платформы. Он проживает возле Сиэтла, штат Вашингтон.



23.12.2011

Параллельные коллекции облегчают разработку многопоточных программ, предоставляя потокобезопасные, удачно сделанные структуры данных. Однако в некоторых случаях разработчику нужно сделать еще один шаг вперед и подумать о регулировании и/или ограничении выполнения потоков. Так как пакет java.util.concurrent призван упростить многопоточное программирование, можно надеяться, что в него включены утилиты для синхронизации. Так оно и есть.

В этой статье, продолжении части 1, мы познакомимся с несколькими конструкциями синхронизации, такими как блокировки и заслонки, предоставляющими более высокий уровень абстракции, чем основные примитивы языка (мониторы), который, однако, недостаточно высок, чтобы поместить их внутрь класса Collection. Использовать их достаточно просто, если вы знаете, для чего они нужны.

Об этой серии статей

Вы думаете, что знаете, как программировать на Java? На самом деле большинство разработчиков лишь поверхностно знакомятся с платформой Java, узнавая лишь то, что необходимо для выполнения работы. В этом цикле, статей Тед Ньювард углубляется в основную функциональность платформы Java, чтобы рассказать о малоизвестных фактах, знание которых может помочь при решении самых сложных задач программирования.

1. Семафор

В некоторых корпоративных системах разработчикам нередко нужно ограничивать количество открытых запросов (потоков/действий), использующих определенный ресурс. На самом деле такое ограничение может иногда улучшить производительность системы, снижая конкуренцию за данный ресурс. Конечно, можно попытаться вручную написать осуществляющий регулировку код, однако легче использовать класс semaphore, который позаботится о регулировке за вас. Рассмотрим пример из листинга 1.

Листинг 1. Ограничиваем потоки с помощью класса Semaphore
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();
    }
}

Хотя в этом примере выполняется 10 потоков (в чем можно убедиться, выполнив для процесса, в котором работает SemApp, команду jstack), только три из них являются активными. Остальные семь вынуждены ждать, пока какой-нибудь из выполняющихся потоков не освободит семафор. (В действительности класс Semaphore поддерживает захват и освобождение более чем одного разрешения за раз, но в данном сценарии это не имеет смысла).


2. CountDownLatch

Если Semaphore – это параллельный класс, предназначенный для того, чтобы позволить потокам "заходить по одному" (возможно, напоминая "вышибал" из популярных ночных клубов), то CountDownLatch напоминает стартовый барьер на скачках. Этот класс задерживает все потоки до тех пор, пока не будет выполнено определенное условие. При выполнении условия он освобождает все потоки одновременно (листинг 2).

Листинг 2. CountDownLatch: Устроим скачки!
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)
                        {
                            // через 0-2 секунды....
                            Thread.sleep(rand.nextInt(3) * 1000);
                            
                            // ... лошадь проходит дистанцию 0-14 пунктов
                            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();
    }
}

Обратите внимание, что в листинге 2 "защелки" CountDownLatch служит двум целям. Во-первых, одна защелка освобождает все потоки одновременно, имитируя старт скачек, однако позднее другая защелка имитирует окончание гонки, чтобы основной поток мог вывести на экран результаты. Чтобы добавить в "скачки" большее количество комментариев, можно добавить защелки CountDownLatch на промежуточных этапах, когда "лошади" проходят четверть, половину и три четверти дистанции.


3. Executor

Довольно досадным изъяном примеров в листинге 1 и листинге 2 является то, что в них необходимо явно создавать объекты класса Thread. Это представляет повод для беспокойства, так как в некоторых JVM создание потока является дорогостоящей операцией, поэтому гораздо лучше повторно использовать существующие потоки, чем создавать новые. В то же время в других JVM все наоборот: потоки довольно легковесны и поэтому гораздо лучше при необходимости создать новый поток. Конечно, если Мёрфи (как обычно) окажется прав, какой бы подход вы ни предприняли, он окажется неправильным для платформы, на которой в итоге будет разворачиваться приложение.

Экспертная группа спецификации JSR-166 (см. раздел Ресурсы) в некоторой степени предугадала эту ситуацию. Вместо того чтобы вынуждать Java-разработчиков создавать потоки напрямую, они ввели интерфейс Executor – абстракцию для получения новых потоков. Как показано в листинге 3, Executor позволяет создавать потоки без самостоятельного использования оператора new для объекта Thread.

Листинг 3. Executor
Executor exec = getAnExecutorFromSomeplace();
exec.execute(new Runnable() { ... });

Использование Executor имеет основной недостаток всех фабрик: фабрика должна откуда-то брать объекты. К сожалению, в отличие от CLR, в JVM нет стандартного пула потоков, доступного в любой виртуальной машине.

Класс Executor является средством получения объектов, реализующих интерфейс Executor, однако он предлагает только new-методы (например, для создания нового пула потоков); у него нет заранее созданных экземпляров. Поэтому, если вы хотите создавать и использовать в своем коде экземпляры Executor, вам придется делать это самостоятельно. (Или, в некоторых случаях, вы можете использовать экземпляр, предоставляемый выбранным вами контейнером/платформой).

К вашим услугам ExecutorService

Хотя интерфейс удобен тем, что позволяет не беспокоиться, откуда в вашем приложении берутся потоки, в нем отсутствует некоторая функциональность, которую Java-разработчик ожидал бы иметь, например возможность добавить поток, предназначенный для генерирования какого-либо результата, и неблокирующим образом дождаться, когда результат станет доступным. (Это типично для настольных приложений, где пользователь выполняет в UI операцию, требующую доступа к базе данных, которую он может захотеть отменить, если ее выполнение занимает слишком много времени).

Поэтому эксперты группы JSR-166 создали более полезную абстракцию – интерфейс, который моделирует фабрику в виде сервиса для запуска потоков, которой можно управлять коллективно. Например, вместо вызова execute() для каждой задачи можно передать в ExecutorService коллекцию задач и возвратить коллекцию List of Futures, представляющую будущие результаты каждой из этих задач.


4. ScheduledExecutorServices

Интерфейс ExecutorService хорош, однако он не подходит для случая, когда некоторые задачи необходимо делать по расписанию, например выполнять что-то через определенные интервалы времени или в заданное время. Здесь пригодится класс ScheduledExecutorService, расширяющий класс ExecutorService.

Если бы вам было нужно написать команду, отображающую "сердцебиение" программы, которая бы выполнялась каждые пять секунд, с помощью ScheduledExecutorService это можно было бы сделать очень просто, например так, как показано в листинге 4.

Листинг 4. ScheduledExecutorService выполняет 'пинг' по расписанию
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);
    }
}

Как насчет этого? Здесь не нужно заботиться ни о потоках, ни о том, что делать, если пользователь захочет остановить "сердцебиение", не нужно явно обозначать потоки как средство реализации; всеми этими деталями планирования занимается ScheduledExecutorService.

Кстати, если пользователь захочет отменить "сердцебиение", то это можно сделать с помощью экземпляра класса ScheduledFuture, возвращаемого вызовом метода scheduleAtFixedRate. Этот класс не только оборачивает в себя результат (если таковой имеется), но и имеет метод cancel для остановки запланированной операции.


5. Тайм-ауты для методов

Одним из главных преимуществ библиотеки java.util.concurrent перед своими предшественниками, такими как мониторы для блокировки, является возможность задавать тайм-ауты для блокирующих операций (что позволяет избежать взаимных блокировок).

Эти методы почти всегда перегружены парой параметров int/TimeUnit, обозначающих, как долго метод должен ожидать перед остановкой с ошибкой и возвращения управления программе. Такие методы требуют больше работы от разработчика – что делать, если блокировка не была захвачена? Однако они всегда обеспечивают более правильный результат – более безопасный для боевой работы код с меньшим количеством взаимных блокировок. (Больше информации о написании готового к боевой работе кода см. в книге Майкла Нигарда Release It!, указанной в разделе Ресурсы).


В заключение

В пакете java.util.concurrent, особенно в пакетах .locks и .atomic, есть еще много приятных утилит, широко расширяющих классы коллекций. Копните глубже, и вы также найдете полезные структуры управления, такие как CyclicBarrier и другие.

Как и во многих других аспектах платформы Java, вы без особого труда сможете найти инфраструктурный код, который может оказаться необычайно полезным. Когда вы пишете многопоточный код, не забывайте об утилитах, которые мы обсуждали в этой и предыдущей статье.

В следующий раз мы раскроем новую тему: пять вещей, которые вы не знали о Jar-архивах.


Загрузка

ОписаниеИмяРазмер
Пример кода для этой статьи5things5-src.zip10 KБ

Ресурсы

  • Познакомьтесь с оригиналом статьи: 5 things you didn't know about ... java.util.concurrent, Part 2 (developerWorks, июнь 2010 г.).(EN)
  • Release it! (Michael Nygard, март 2007 г., Pragmatic Programmers): проектируйте свои приложения так, чтобы максимизировать время их безотказной работы, производительность и оборачиваемость инвестиций.
  • JSR 166: узнайте больше об интерфейсе Executor и других изменениях, привнесенных этим JSR.
  • Теория и практика Java: Параллельные классы коллекций (Брайан Гетц, developerWorks, июль 2003 г.): узнайте, как пакет Дуга Ли util.concurrent дал новую жизнь стандартным типам коллекций List и Map.
  • В статье Spice up collections with generics and concurrency (John Zukowski, developerWorks, апрель 2008 г.) представлены изменения инструментального набора коллекций в Java 6.
  • Package java.util.concurrent, Java platform SE 6: узнайте больше о вспомогательных классах, обсуждаемых в этой статье.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=782753
ArticleTitle= Пять вещей, которые вы не знали о ... пакете java.util.concurrent. Часть 2
publish-date=12232011