И еще немного о многопоточности

Многопоточность и Java

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

Мы с вами в прошлый раз, напомню, рассмотрели создание многопоточных приложений с использованием Delphi. В той статье, к сожалению, обнаружились некоторые неточности, за которые я приношу глубочайшие извинения читателям. Дело в том, что метод Synchronize на самом деле не занимается синхронизацией потоков, а всего лишь заставляет код выполняться в главном потоке. Трудно сказать, зачем создатели Delphi решили назвать функцию именно так (думаю, не один я был введён в заблуждение её названием). Тем не менее, код в прошлой части статьи полностью рабочий, хотя и метод Synchronize истолкован мною ошибочно, за что позвольте ещё раз принести вам свои извинения. Возможно, мы ещё вернёмся к проблеме синхронизации в Delphi, но сейчас, как и было обещано, - Java.

Delphi - язык программирования, конечно, хороший, однако не слишком востребованный в наше время, да и местами (как в случае с Syncronize) не слишком последовательный. Другое дело - Java. Виртуальные доски пестрят объявлениями о поиске Java-программистов, SourceForge.net заполнен проектами, написанными на этом языке программирования, да и возможностей у Java сегодня побольше, чем у Delphi. Хотя, конечно, считать возможности того или иного языка программирования - дело неблагодарное. Даже на Brainfuck'е (см. статью "Лосиные губы в яблочном уксусе" в "КВ" №5/2007) можно программировать, ежели есть такое желание. Скажем так, Delphi и Java являются весьма отличающимися друг от друга языками программирования, из чего и надо исходить, обсуждая их возможности. Java, в силу специфики наиболее актуальных в наши дни задач, более популярен и распространён сегодня, но это вовсе не означает, что Delphi чем-то хуже.

Впрочем, сейчас мы всё же говорим не о Java и Delphi, а о многопоточных приложениях. Поэтому давайте ими и займёмся.

 


Некоторые особенности

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

С самого начала Java задумывался как язык программирования, позволяющий легко и быстро писать многопоточные приложения. Поэтому поддержка их создания реализована, в отличие от того же Delphi, не на уровне стандартной библиотеки, а куда глубже - на уровне самого языка. Хотя, конечно, без стандартной библиотеки классов мы при программировании на Java тоже не обойдёмся - она интегрирована в язык настолько глубоко, что реально писать приложения без её использования вряд ли кому-то удастся.

Все приложения, которые мы пишем на Java, используют, конечно же, классы. Соответственно, и потоки, как и в случае в Delphi, также будут классами. Создавать эти классы можно двумя способами: либо наследовать от класса Thread, либо реализовывать интерфейс Runnable. Возможность выбора сразу порождает вопрос: а что же из этого более предпочтительно? Ответ на него зависит, конечно, от конкретной ситуации, в которой мы оказались. Если мы пишем приложение с нуля и заранее закладываем в его архитектуру многопоточность, то, наверное, здесь мы оказываемся перед выбором одного из двух совершенно идентичных по своей злости зол. Но когда нам надо "задним числом" реализовать многопоточность в приложении, которое написано в расчёте на использование только одного потока выполнения, то здесь уже всё, конечно, несколько меняется, и возможность выбора испаряется прямо на глазах. Конечно, в этом случае нам намного более удобно будет реализовать в уже имеющихся классах поддержку интерфейса Runnable - ведь по причине отсутствия множественного наследования в Java мы просто не сможем унаследовать класс Thread в том случае, если наш класс-поток уже является наследником какого-либо другого класса. Интерфейсов же мы можем реализовать в одном классе сколько угодно (теоретически, конечно, их количество наверняка ограничено, но, скорее всего, каким-нибудь числом, которое нам будет очень трудно превысить).


Практика? А вот и она!

Мы сейчас с вами остановимся именно на использовании интерфейса Runnable, поскольку этот способ более общий - мы можем использовать его как в новых проектах, так и при переделке старых, и при этом получать именно тот результат, на который и рассчитываем. Хотя, в общем-то, особых отличий от использования класса Thread не будет.

Что касается того, чем будет заниматься наше приложение, которое мы разберём как пример, то здесь я решил особо не оригинальничать и воспользоваться примером, который приводится во многих книгах по Java. Это приложение, занимающееся скачкой HTML-страниц по заданному адресу. Один поток занимается их непосредственно закачкой на жёсткий диск локального компьютера, а второй в это время, само собой, тоже не сидит без дела - он занимается также весьма полезной задачей, а именно - разбором скачиваемых страниц на предмет поиска новых ссылок. Эти ссылки добавляются в специальную очередь, из которой их потом берёт поток, занимающийся скачиванием файлов, ну и, соответственно, они закачиваются и сохраняются. Приложение, в общем-то, по своему объёму должно было бы получиться довольно большим, а потому большую часть его функциональности мы с вами опустим, поскольку нас будет интересовать сейчас исключительно и только то его свойство, что оно у нас будет многопоточным. Конечно, было бы более чем полезно рассмотреть такое приложение всё целиком, однако, сами понимаете, газетные полосы бумажные, а не резиновые, соответственно, и статья имеет определённые ограничения по объёму.

Давайте посмотрим на программный код в листинге, а затем уже займёмся его разбором и обсуждением.

public class Searcher implements Runnable {
 protected ConcurrentLinkedQueue<String> processing;
 public void run(){
  try{
   // Здесь код, который занимается парсингом страниц
  }
  catch (Exception e)
  {
   System.out.println(e.getMessage());
  };
 }
 public Searcher (){
  processing = new ConcurrentLinkedQueue<String>();
 }
}

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


Синхронизация, как же без неё

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

public boolean download(String start) {
 //...
 do{
  synchronized (semaphore){ 
   //...
  }
 }while (!downloads.isEmpty());
}

Здесь код, конечно, получился ещё более абстрактным, и ещё меньше верится, что он может как-либо использоваться для закачки каких-то файлов, но суть сейчас не в этом. А в чём тогда? А в том, что в начале участка кода, который подлежит синхронизации, мы применяем специальный оператор языка Java под названием synchronized. В скобках после него указан объект, который будет использоваться в процессе синхронизации (у нас этот объект назван semaphore). Какой объект может быть использован для синхронизации? Вопрос, конечно, хороший, однако ответить на него просто. Оператор synchronized спроектирован в Java таким образом, что позволяет использовать для синхронизации любой класс. Работает это следующим образом: когда выполнение потока подходит к тому месту, где его ожидает синхронизируемый код, он будет производить вызов всех методов указанного объекта лишь после того, как будет успешно выполнен вход в однопоточную область выполнения программы. Получается это всё благодаря специфическому механизму работы с потоками в Java. В этом языке каждый класс неявно реализует в себе монитор - так в терминах Java называют семафоры. В заданный момент времени монитор может быть лишь у одного экземпляра из всех поточных объектов, и когда поток получает права на эксклюзивные действия в синхронизируемой области кода, все остальные потоки остаются ждать за её пределами до тех пор, пока тот поток не выйдет из данной области, и им не будет подан сигнал о том, что можно также в неё входить. В общем-то, мы с вами уже рассматривали этот механизм, но преимущество его реализации в Java состоит в том, что этот язык программирования требует от нас минимальных телодвижений при работе с мониторами - львиную долю всей работы язык делает автоматически.

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


Резюме

Что ж, давайте подведём итог всему тому, что написано выше, и вообще нашему с вами разговору о многопоточности.

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

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

Вадим СТАНКЕВИЧ

Версия для печатиВерсия для печати

Номер: 

46 за 2008 год

Рубрика: 

Software
Заметили ошибку? Выделите ее мышкой и нажмите Ctrl+Enter!
 

Комментарии

Страницы

Аватар пользователя Логик
mr > незнаю кого вы процитировали,

Гельберд Шилдт. "Java 2 v 5.0 TIGER (новые возможности)" стр. 192.

Гельберд Шилдт.- "всемирно известный автор и вед. специалист по С, С++, Java и C#... продано более 3 миллиона его книг... переведены на все основные языки народов мира..."

Аватар пользователя Логик
mr > кстати на собеседованиях часто задаю вопросы про пулы потоков, таймауты, синхронизацию и внятных ответов не получаю

Это понятно. :-)

>про concurrent пакеты никто из собеседуемых пока не знал.

И не надо знать, пока не попросят применить. Ну, типа, надо, может быть, помнить, про то, что такие пакеты есть вообще-то, но не более того. имхо

Аватар пользователя mr
ну книжки шилда в it это как донцова в области детективов - исключительно для новичков )

вроде у этого писателя нет серьёзной консалтинговой деятельности в it

лучше брюса эккеля почитать - он "пробовал устриц"

Аватар пользователя Логик
mr > лучше брюса эккеля почитать - он "пробовал устриц"

У меня на брюса эккеля идиосинкразия . На его стиль написания.

Аватар пользователя Eugene
Чуваки, не ссорьтесь :)))
Аватар пользователя Логик
>Чуваки не ссортесь :)))

Разве мы ссорились?

Аватар пользователя Anatol
Господа, простите за то, что я, правтически совершенно не соображающий в программировании, выскажусь.

Не вдаваясь в тонкости, скажу, что утверждение, что многоядерность даёт лишь проценты производительности, просто неверное. Практически вот AutopanoPro при выставлении в настройках двух процессоров и двух потоков даёт увеличение скорости обработки изображений в разы. То есть да, нужны специально написанные программы. Эффект - большой.

Аватар пользователя Anatol
Тю, экий я невнимательный, говорил-то я о статье о многопроцессорнности, а это не та. Сначала написал, а потом решил глянуть ещё раз статью. Ну и увидел.

Так ещё больший миль пардон!

Страницы