Многопоточность и 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 в методе попросту невозможно.
Резюме
Что ж, давайте подведём итог всему тому, что написано выше, и вообще нашему с вами разговору о многопоточности.
Как видите, на практике современные языки программирования позволяют создавать многопоточные приложения, не слишком задумываясь в процессе их написания о внутренней кухне взаимодействия потоков друг с другом. То есть, потоки могут совершенно спокойно взаимодействовать даже тогда, когда программист сам не занимается ни написанием семафоров, ни рассылкой сообщений потокам, ни прочими низкоуровневыми, с точки зрения многопоточного программирования, вещами.
Впрочем, это не делает необязательным понимание механизма работы многопоточных приложений, поскольку без этого довольно сложно работать с потоками тогда, когда их становится больше двух и когда они делают более-менее полезную работу, подразумевающую их интенсивное "общение" друг с другом.
Вадим СТАНКЕВИЧ
Комментарии
Страницы
Это верно. Но это только описание одного из нескольких состояний треда(потока). Второе состояние - это выполнение потока. :-)
Но кроме этих двух состояний, поток может находиться в четырех(!) других состояниях. - Так что ждем, Вадим, продолжения?! :-)
Есть одна тонкость. Можно заблокировать доступ любого потока к объекту с атрибутом метода synchronized ВООБЩЕ НЕ ИСПОЛЬЗУЯ(НЕ ВЫЗЫВАЯ)ЭТОТ МЕТОД!
Например, если объект semaphore имеет один(два, три...) метода с атрибутом synchronized: semaphore.metod1() semaphore.menod2(), то использование кода типа:
synchronized (semaphore){
//...
}
при том, что внутри блока {} вообще даже НЕ упоминается объект semaphore, приведет к тому, что никакой другой поток просто не сможет вызвать(станет в очередь на вызов) методы semaphore.metod1() semaphore.menod2()НИГДЕ в то время, пока не произойдет выход нашего потока из вышенаписанного блока {}.
Таким образом мы можем заблокировать потоки, даже при том, что ни один из этих потоков даже не стал выполнять метод с атрибутом synchronized.
synchronized (semaphore-flag){
//...
}
от такой же конструкции, когда вы хотите на все время нахождения в блоке {} закрыть, например, изменения состояния объекта:
synchronized (semaphore-gruppa){
//...
semaphore-gruppa.readMembers()
//...
}
Во втором случае, в этом блоке {} вы можете "долго" выполнять различные манипуляции с объектом semaphore-gruppa - читать его членов, подсчитывать его членов, составлять отчет по его членам и прочее, не опасаясь, что объект semaphore-gruppa кто-то изменит (добавит члена) где-то в другом потоке, при условии, конечно, что все методы на изменение объекта semaphore-gruppa будут иметь атрибут synchronized.
Тогда, находясь в этом блоке {} можно как угодно "долго" работать с методами объекта semaphore-gruppa (конечно, только теми методами, которые имеют атрибут synchronized) , не опасаясь, что в момент между "выходом", как обычно, из одного synchronized метода этого объекта и "заходом" в другой synchronized метод этого же объекта какой-либо другой поток успеет поменять состояние этого объекта semaphore-gruppa.
Обратимся к первоисточникам.
Здесь ясно, доступно и по русски про синхрнизацию в Java. Просто уделите 20 мин времени и вы убедитесь в бесполезности вашего спора
тыдыщ: http://ru.sun.com/pdf/java-course/Java_COURSE_Lec12.pdf
Разве мы ссорились?
- богатство пакетов java.util.concurrent.*
- "волшебное слово" volatile
- java.util.concurrent.*
Кратко. java.util.concurrent.* - не использовать. новичкам. имхо.
А когда использоывать java.util.concurrent.* ? - когда скажут. не ранее. имхо.
новичкам как раз и надо разбираться и использовать эти пакеты - меньше ошибок будет
нет. эти пакеты "для систем требующих большого распараллеливания или для специализированных приложений."
новички обязаны освоить synchronized и использовать его. имхо
- что считать "большим распараллеливанием"?
- "специализированные приложения" - это надеюсь не там где потоки есть?
вот цитата из jdk
Utility classes commonly useful in concurrent programming. This package includes a few small standardized extensible frameworks, as well as some classes that provide useful functionality and are otherwise tedious or difficult to implement.
из своего опыта - не представляю сколько новичёк потратит на ту же грамотную поддержку таймаутов вызовов без использования этих пакетов.
кстати на собеседованиях часто задаю вопросы про пулы потоков, таймауты, синхронизацию и внятных ответов не получаю, про concurrent пакеты никто из собеседуемых пока не знал.
Страницы