Многопоточность в среде Java*

Многопоточность в среде Java*

Автор: Алан Макнотон (Allan McNaughton)

Организация потоков в среде Java* позволяет добиться прироста производительности практически во всех приложениях. Узнайте, как многопоточность становится стандартным средством программирования в среде Java*, благодаря технологии Hyper-Threading и возможностям новых многоядерных процессоров Intel®.

Разработка приложений на основе потоков, то есть особых последовательностей команд, обеспечивает значительный прирост производительности. Рассмотрим, например, программу-проигрыватель DVD, которая считывает большие объемы данных с компакт-диска, обрабатывает их и выводит изображение на экран. Обычные однопоточные программы – а это большинство современных клиентских программ – выполняют перечисленные действия последовательно. В данном примере обработка данных начнется лишь после того, как с диска будет считан определенный объем информации. Производительность такой программы невысока: ресурсы, которые могут использоваться для обработки, не будут задействованы, пока идет считывание следующей порции информации с диска.

В многопоточной версии проигрывателя один поток предназначался бы для считывания информации, второй – для обработки, а третий – для записи данных в видеобуфер, причем все они выполнялись бы параллельно, а обработка продолжалась бы даже во время чтения с диска – прирост производительности налицо! Можно привести еще много примеров, когда одновременное выполнение нескольких действий приводит к повышению производительности системы. Именно для увеличения производительности и реализована многопоточность в виртуальной машине Java* (JVM).

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


Организация многопоточности в коде Java

При запуске любой программы образуется, по крайней мере, один поток. В С/С++ и Java этот поток запускается при вызове метода main(). Для создания дополнительных потоков необходимо их запустить и назначить им определенные функции, после выполнения которых потоки автоматически будут завершены виртуальной машиной.

В среде Java для создания потока и назначения им функций есть два способа. Первым из них является наследование от класса Thread (входящего в пакет java.lang) с последующим переопределением метода run() на функцию, которую вы хотите назначить для потока. Ниже приведен пример кода:

public class SimpleThread extends Thread {
public SimpleThread(String str) {
super(str);
}
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i + " " + getName());
try {
sleep((long)(Math.random() * 1000));
} catch (InterruptedException e) {}
}
System.out.println("DONE! " + getName());
}
}


Таким образом, происходит наследование от класса Thread с запуском переопределенного метода run(). Назначенная функция организует цикл, при котором на экран выводится строка, после чего программа останавливается на случайный интервал времени. После 10 итераций цикла на экран выводится слов "DONE!", а поток завершается, так как функция закончила свое выполнение. Ниже приведен пример функции для создания потока:

public class TwoThreadsDemo {
public static void main (String[] args) {
new SimpleThread("Do it!").start();
new SimpleThread("Definitely not!").start();
}
}



Код прост: запускается функция с именем, которое будет выводиться на экран, а затем вызывается метод start(), который, в свою очередь, вызовет метод run(). Результат выполнения программы несложно предугадать:

0 Do it!
0 Definitely not!
1 Definitely not!
2 Definitely not!
1 Do it!
2 Do it!
3 Do it!
3 Definitely not!
4 Do it!
4 Definitely not!
5 Do it!
5 Definitely not!
6 Do it!
7 Do it!
6 Definitely not!
8 Do it!
7 Definitely not!
8 Definitely not!
9 Do it!
DONE! Do it!
9 Definitely not!
DONE! Definitely not!


Как видно из примера, результаты обработки двух потоков выводятся в смешанном порядке. Если программа будет выполняться в один поток, сначала на экран выведутся все строки со словами "Do it!", а затем – строки со словами "Definitely not!".

Результаты повторных запусков этой программы будут отличаться, потому что пауза между итерациями цикла случайна и, главное, не установлена синхронизация между потоками. JVM будет запускать потоки по порядку, при котором они будут обрабатываться быстрее всего, но при этом неизвестно, когда именно запустится определенный поток. Поэтому в случае необходимости можно установить приоритет для потоков, которые JVM должна обработать первыми.

Второй способ создания потока – использование класса с реализацией интерфейса Runnable (также из пакета java.lang). Этот интерфейс определит метод run(), который будет выполнять основную функцию потока, аналогично описанному выше первому способу.

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


Реализация многопоточности

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

Ожидание завершения другого потока: Предположим, нам нужно обработать целочисленный массив. Это можно делать последовательно, обрабатывая одно значение массива за другим, или, что более продуктивно, параллельно – созданием нескольких потоков, каждому из которых назначена определенная область массива. Допустим, чтобы перейти к следующей стадии обработки, нам необходимо дождаться завершения всех потоков. Для синхронизации взаимодействия потоков используется метод join(). При его задействовании запуск определенного потока произойдет после завершения другого. Таким образом, поток В будет ждать завершения потока А. При превышении времени ожидания, назначенного в методе join(), поток В займется обработкой других данных, если поток А еще не завершен. Итак, мы обсудили самую сложную задачу при организации многопоточности – проблему ожидания завершения потоков. Вернемся к ней позже.

Ожидание доступа к заблокированным объектам: Предположим, мы занимаемся разработкой системы бронирования билетов для аэропорта. Чаще всего многопоточность в таких программах организована таким образом, что каждому подключенному к системе пользователю, а также всем кассирам назначены отдельные потоки (организация очень больших систем может отличаться от описанной). Если два пользователя одновременно попытаются забронировать одно и то же место, а в программе эта возможность не предусмотрена, произойдет следующее: один поток обратится к конкретному ресурсу одновременно с другим потоком. В результате оба пользователя решат, что они забронировали себе нужный билет на самолет.

Для того чтобы предотвратить изменение одних и тех же данных разными потоками, необходимо, чтобы один поток заблокировал доступ к ним для другого потока. Тогда при попытке второго потока изменить данные, ему придется ждать, пока первый поток не снимет свою блокировку. В нашем примере второй поток обнаружит, что данное место уже было зарезервировано, и выдаст соответствующее предупреждение пользователю. Ситуация, описанная выше, называется "состоянием гонки" (race condition), и ее последствия могут быть очень пагубными. Вот почему на время выполнения операций с данными одним потоком принято блокировать доступ к этим данным для других потоков.

В среде Java блокировку можно реализовать несколькими способами. Наиболее частым является использование ключевого слова "synchronized". Метод, содержащий это ключевое слово, может выполняться только одним потоком, а после выполнения метода блокировка снимается. Рассмотрим пример:

protected synchronized int reserveSeat ( Seat seat_number )
{
if ( seat_number.getReserved() == false )
{
seat_number.setReserved();
return ( 0 );
}
else
return ( -1 );
}


В данном случае одновременное выполнение метода несколькими потоками невозможно – состояние гонки предотвращено блокировкой.

Использование синхронизации – не единственный метод организации взаимодействия потоков. В среде Java версии J2SE* 5.0 доступно еще несколько удобных способов блокировки. Большинство из них находятся в пакете java.util.concurrent.locks, который стоит внимательно изучить для уверенной работы с потоками в среде Java.

Как отмечалось выше, механизмы блокировки помогают предотвратить состояние гонки, но с ними связано еще несколько проблем, например, тупиковая ситуация (deadlock). Она заключается в следующем: предположим, поток А ожидает завершения потока В, который, в свою очередь, ожидает завершения потока А. Таким образом, оба потока будут всегда заблокированы. Возможность образования тупиковых ситуаций трудно предугадать, поэтому очень важно, чтобы между потоками не было зависимостей подобного рода.


Использование пулов потоков

Выше упоминалось, что потоки после выполнения своих функций автоматически завершаются виртуальной машиной. При этом в выделенных им областях памяти остается ненужная информация. Что она собой представляет? Это служебная информация, которую процессору приходится обрабатывать при каждом создании и завершении потока. Поэтому для оптимизации многопоточности чаще всего выделяется группа (которую называют пулом потоков), потоки из которой используются многократно. В таком случае функция, соответствующая потоку, записана в пуле и ждет задачи для выполнения. После выполнения полученной задачи поток возвращается в пул.

Специально для организации пула потоков предназначена среда из состава пакета java.util.concurrent, входящего в J2SE 5.0. Дополнительную информацию об организации пулов потоков в среде Java, в том числе подробное руководство, можно найти по адресу http://java.sun.com/developer/JDCTechTips/2004/tt1116.html#2*

При разработке многопоточных программ и создании пулов потоков возникает еще один вопрос: количество потоков. Оно зависит от того, как вы планируете использовать свои потоки. Если потоки предназначены для выполнения нескольких отдельных задач, создайте столько потоков, сколько задач необходимо обработать. Например, в текстовом процессоре один поток должен использоваться для вывода на экран (практически во всех программах главный поток предназначен для обновления интерфейса пользователя), другой – для разбиения документа на страницы, третий – для проверки правописания, четвертый – для каких-то фоновых функций. То есть в данном случае оптимальное количество потоков – четыре; это делает разработку ПО значительно проще.

Однако если подобная программа использует несколько потоков для выполнения похожих задач, оптимальное количество потоков зависит от ресурсов системы, особенно числа конвейеров процессора и количества процессоров. В современных системах на базе процессоров Intel с поддержкой технологии Hyper-Threading установлены двухъядерные процессоры, а для каждого процессорного ядра выделено два конвейера. Специалисты Intel считают, что процессоры следующего поколения будут иметь еще больше ядер, а, следовательно, возрастет и производительность вычислительных систем, в то время как тепловыделение и энергопотребление вырастут незначительно. Конечно, увеличится и количество конвейеров.

Учитывая, что каждое ядро имеет в своем распоряжении два конвейера, нетрудно подсчитать, что для достижения оптимальной производительности при работе в системе на базе двухъядерных процессоров Pentium® 4 необходимо использовать четыре потока. Аналогично, для рабочих станций на базе процессоров Intel® Xeon® оптимальной будет организация четырех потоков, так как эти процессоры поддерживают технологию Hyper-Threading, но не являются многоядерными.


Заключение

При запуске на платформах Intel многопоточных программ, которые были разработаны в среде Java, вы наверняка захотите проконтролировать загрузку процессоров и выполнение потоков. Один одна из оптимальных виртуальных машин для этой цели – BEA's WebLogic* JRockit*, специально разработанная инженерами компаний BEA и Intel для платформ Intel.

Независимо от используемой вами JVM, анализатор производительности Intel® VTune™ поможет проконтролировать выполнение программного кода и обнаружить в нем узкие места, влияющие на производительность. Документацию по использованию анализатора производительности Intel VTune можно найти здесь [PDF 2 Мб].

В данной статье рассматривалась организация многопоточности на платформах Java. По мере роста продаж процессоров Intel с поддержкой технологии Hyper-Threading и выпуска новых многоядерных процессоров поддержка многопоточности станет обязательным требованием к новым приложениям, производительность которых возрастет за счет повышения количества вычислительных конвейеров, которое, в свою очередь, увеличивается пропорционально количеству ядер процессора. В статье обсуждается многопоточность – технология, которую должны поддерживать приложения, чтобы иметь возможность использовать все вычислительные мощности новых систем. Приводятся подтверждения прироста производительности многопоточных приложений, созданных в среде Java. Ниже приведен список дополнительных ресурсов, в которых раскрываются некоторые вопросы многопоточности.


Справочные материалы и дополнительные ресурсы

Замечательное руководство по многопоточному программированию в среде Java вы можете найти по адресу: http://java.sun.com/docs/books/tutorial/essential/threads/*

Документацию по средствам для организации многопоточности в среде J2SE 5.0 вы можете найти по адресу: http://java.sun.com/j2se/1.5.0/docs/guide/concurrency/*

Подробная информация о BEA JRockit доступна по адресу http://dev2dev.bea.com/products/wljrockit81/index.jsp

Главная страница анализатора производительности Intel® VTune™: http://www.intel.com/software/products/vtune/index.htm

Автор статьи рекомендует прочесть работу, в которой описаны все возможности многопоточности в среде J2SE 5.0: Java Threads (Организация потоков в среде Java), 3-е издание. Авторы: Скотт Оакс (Scott Oaks) и Генри Вонг (Henry Wong). Издательство O’Reilly and Associates, ISBN 0-596-00782-5.


Об авторе

Алан Макнотон – технолог, обладатель нескольких патентов, писатель, обладающей более чем 15-летним опытом работы в отрасли. Он является президентом Technical Insight LLC – компании, которая специализируется на издании документации по высоким технологиям. Макнотон часто публикует и свои статьи по вопросам передовых технологий.


Para obter informações mais completas sobre otimizações do compilador, consulte nosso aviso de otimização.