Многозадачность в javame при http соединении


Содержание

Многопоточное программирование

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

Класс Thread

В Java функциональность отдельного потока заключается в классе Thread . И чтобы создать новый поток, нам надо создать объект этого класса. Но все потоки не создаются сами по себе. Когда запускается программа, начинает работать главный поток этой программы. От этого главного потока порождаются все остальные дочерние потоки.

С помощью статического метода Thread.currentThread() мы можем получить текущий поток выполнения:

По умолчанию именем главного потока будет main .

Для управления потоком класс Thread предоставляет еще ряд методов. Наиболее используемые из них:

getName() : возвращает имя потока

setName(String name) : устанавливает имя потока

getPriority() : возвращает приоритет потока

setPriority(int proirity) : устанавливает приоритет потока. Приоритет является одним из ключевых факторов для выбора системой потока из кучи потоков для выполнения. В этот метод в качестве параметра передается числовое значение приоритета — от 1 до 10. По умолчанию главному потоку выставляется средний приоритет — 5.

isAlive() : возвращает true, если поток активен

isInterrupted() : возвращает true, если поток был прерван

join() : ожидает завершение потока

run() : определяет точку входа в поток

sleep() : приостанавливает поток на заданное количество миллисекунд

start() : запускает поток, вызывая его метод run()

Мы можем вывести всю информацию о потоке:

Первое main будет представлять имя потока (что можно получить через t.getName() ), второе значение 5 предоставляет приоритет потока (также можно получить через t.getPriority() ), и последнее main представляет имя группы потоков, к которому относится текущий — по умолчанию также main (также можно получить через t.getThreadGroup().getName() )

Недостатки при использовании потоков

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

На некоторых платформах запуск новых потоков может замедлить работу приложения. Что может иметь большое значение, если нам критичная производительность приложения.

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

Многозадачность в Andro > 31.08.2020 в 21:20

Случай 1. Выполнение запроса по сети, без необходимости ответа от сервера

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

Т.к. это включает в себя создание запроса по сети, вы должны сделать это из потока, отличного от основного.

Вариант 1. AsyncTask или загрузчики

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

Однако AsyncTask и загрузчики зависят от жизненного цикла активности. Это означает, что вам нужно будет либо дождаться завершения вызова, либо попытаться помешать пользователю покинуть активность или надеяться, что он будет выполнен до того, как активность будет уничтожена.

Вариант 2. Service

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

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

Это потребует больше усилий, чем это необходимо для такого простого действия.

Вариант 3. IntentService

Это, на мой взгляд, было бы лучшим вариантом.

Поскольку IntentService не привязан к какой-либо активности и работает на потоке, отличном от UI, он отлично удовлетворяет нашим потребностям. Кроме того, IntentService автоматически останавливается, поэтому нет необходимости вручную управлять им.

Случай 2. Выполнение сетевого вызова и получение ответа от сервера

Этот вариант использования, вероятно, более распространен. Например, вы можете вызвать API в фоновом режиме и использовать его ответ для заполнения полей на экране.

Вариант 1. Service или IntentService

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

Вариант 2. AsyncTask или загрузчики

AsyncTask или загрузчики выглядели бы здесь очевидным решением. Они просты в использовании — просты и понятны.

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

Это довольно много работы для каждого вызова. К счастью, теперь доступно гораздо лучшее и простое решение: RxJava.

Вариант 3. RxJava

Возможно вы слышали о библиотеке RxJava, разработанной Netflix. Это почти волшебство на Java.

RxAndroid позволяет использовать RxJava в Android и позволяет работать с асинхронными задачами. Вы можете узнать больше о RxJava на Android здесь. RxJava предоставляет два компонента: Observer и Subscriber .

Наблюдатель — это компонент, который содержит какое-то действие. Он выполняет это действие и возвращает результат, если он удался, ошибка, если он не работает.

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

В RxJava вы сначала создаете наблюдателя:

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

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

Вы связываетесь с наблюдателем этими двумя функциями:

Планировщики — это компоненты, которые выполняют действие в определенном потоке. AndroidSchedulers.mainThread() — это планировщик, связанный с основным потоком.

Учитывая, что наш вызов API — это mRestApi.getData() , и он возвращает объект Data, базовый вызов может выглядеть так:

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

Случай 3. Цепочка сетевых вызовов

Для сетевых вызовов, которые необходимо выполнить последовательно (т.е. когда каждая операция зависит от ответа / результата предыдущей операции), вам нужно быть особенно осторожным при создании спагетти кода.

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

Вариант 1. AsyncTask или загрузчики

Использование AsyncTask или загрузчиков почти наверняка приведет к спагетти коду. Общую функциональность будет трудно реализовать правильно, и для вашего проекта потребуется огромное количество избыточного шаблонного кода.

Вариант 2. RxJava с использованием flatMap

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

Шаг 1. Создайте наблюдателя, который извлекает токен:

Шаг 2. Создайте наблюдателя, который получает данные с помощью токена:

Шаг 3. Цепочка двух наблюдателей вместе и подписка:

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

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

Случай 4. Общение c UI потоком из другого потока

Рассмотрим сценарий, в котором вы хотите загрузить файл и обновить пользовательский интерфейс после его завершения.

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

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

Вариант 1. RxJava внутри сервиса

RxJava, как самостоятельно, так и внутри IntentService , может быть не идеальным решением. Вам нужно будет использовать механизм обратного вызова при подписке на Observable , а IntentService построен для выполнения простых синхронных, а не обратных вызовов.

С другой стороны, с помощью Service вам необходимо будет вручную остановить службу, что потребует дополнительной работы.

Вариант 2. BroadcastReceiver

Android предоставляет компонент, который может слушать глобальные события (например, события батареи, сетевые события и т.д.), А также настраиваемые события. Вы можете использовать этот компонент для создания настраиваемого события, которое запускается при завершении загрузки.

Для этого вам нужно создать собственный класс, который расширяет BroadcastReceiver , регистрирует его в манифесте и использует Intent и IntentFilter для создания настраиваемого события. Чтобы инициировать событие, вам понадобится метод sendBroadcast .

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

Вариант 3. Использование Handler

Handler — это компонент, который может быть присоединен к потоку, а затем быть использованным для выполнения некоторых действий в этом потоке с помощью простых сообщений или Runnable задач. Он работает совместно с другим компонентом — Looper , который отвечает за обработку сообщений в конкретном потоке.

Когда Handler создается, он может получить объект Looper в конструкторе, который указывает, к какому потоку он прикреплен. Если вы хотите использовать Handler, прикрепленный к основному потоку, вам нужно использовать looper, связанный с основным потоком, вызывая Looper.getMainLooper() .

В этом случае для обновления пользовательского интерфейса из фонового потока вы можете создать Handler, подключенный к потоку пользовательского интерфейса, а затем опубликовать действие как Runnable :


Этот подход намного лучше, чем первый, но есть еще более простой способ сделать это…

Вариант 3. Использование EventBus

EventBus — это популярная библиотека от GreenRobot, позволяет компонентам безопасно связываться друг с другом. Поскольку наш вариант использования — это тот, где мы хотим обновить интерфейс, это может быть самым простым и безопасным способом.

Шаг 1. Создайте класс событий. Например, UIEvent .

Шаг 2. Подпишитесь на событие.

Шаг 3. Отправьте событие: EventBus.getDefault (). Post (новый UIEvent ());

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

Вы можете структурировать свой класс UIEvent , чтобы он содержал дополнительную информацию по мере необходимости.

В активности / фрагменте:

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

Случай 5. Двусторонняя связь между потоками на основе действий пользователя

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

Полный пример медиаплеера выходит за рамки этой статьи. Однако вы можете найти хорошие материалы здесь и здесь.

Вариант 1. Использование EventBus

Здесь вы можете использовать EventBus . Однако, как правило, не безопасно отправлять событие из потока пользовательского интерфейса и получать его в сервисе. Это связано с тем, что вы не можете узнать, работает ли сервис, когда вы отправили сообщение.

Вариант 2. Использование BoundService

BoundService — это служба, связанная с активностью / фрагментом. Это означает, что активность / фрагмент всегда знает, работает служба или нет и получает доступ к публичным методам службы.

Чтобы реализовать его, вам необходимо создать настраиваемый Binder внутри службы и создать метод, который возвращает службу.

Чтобы связать активность со службой, вам необходимо реализовать ServiceConnection , который является классом, контролирующим статус службы, и использовать метод bindService для привязки:

Здесь вы можете найти полный пример реализации.

Чтобы связаться со службой, когда пользователь нажимает кнопку «Воспроизведение» или «Пауза», вы можете привязать их к службе, а затем вызвать соответствующий общедоступный метод в службе.

Когда есть мультимедийное событие, и вы хотите сообщить об этом активности / фрагменту, вы можете использовать один из более ранних подходов (например, BroadcastReceiver, Handler или EventBus).

Случай 6. Выполнение действий параллельно и получение результатов

Предположим, вы строите туристическое приложение, и вы хотите показать достопримечательности на карте, полученные из нескольких источников (разные поставщики данных). Поскольку не все источники могут быть надежными, вы можете проигнорировать те, которые потерпели неудачу, и продолжать отображать карту в любом случае.

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

Вариант 1: Использование RxJava

В RxJava вы можете комбинировать несколько наблюдателей в одном с помощью операторов merge() или concat() . Затем вы можете подписаться на «merged» наблюдатели и ждать всех результатов.

Но, этот подход не будет работать должным образом. Если один вызов к API завершится неудачно, объединенный (merged) наблюдатель сообщит об общем сбое.

Вариант 2. Использование Java-компонентов

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

Каждая задача, которую вы отправляете в ExecutorService , должна содержаться в интерфейсе Callable , который является интерфейсом для создания задачи, которая может генерировать исключение.

Получив результаты от invokeAll() , вы можете проверить каждый результат и действовать соответствующим образом.

Скажем, например, что у вас есть три типа данных, поступающих из трех разных конечных точек, и вы хотите сделать три параллельных вызова:

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

Этот подход проще, чем использование RxJava. Это проще, короче и не отменяет все действия из-за одного исключения.

Случай 7. Запрос к локальной SQLite базе данных

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

При запросе данных из SQLite вы получаете объект Cursor , который затем может использоваться для получения фактических данных.

Вариант 1: Использование RxJava

Вы можете использовать RxJava и получать данные из базы данных, так же, как мы получаем данные из нашего back-end:

Вы можете использовать наблюдателя, возвращаемого getLocalDataObservable() следующим образом:

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

Вариант 2: Использование CursorLoader + ContentProvider

Android предоставляет CursorLoader , собственный компонент для загрузки данных из SQLite и управления соответствующим потоком. Это Loader , который возвращает Cursor , который мы можем использовать для получения данных, вызывая простые методы, такие как getString() , getLong() и т.д.

CursorLoader работает с компонентом ContentProvider . Этот компонент предоставляет множество возможностей для работы с базой данных в режиме реального времени (например, уведомления об изменениях, триггеры и т.д.), что позволяет разработчикам более легко реализовать более удобный пользовательский интерфейс.

Заключение

Android предоставляет множество способов обработки и управления потоками, но ни один из них не является серебрянной пулей.

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

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

Многопоточность в Java 8. Часть 2 (Пишем асинхронный код с CompletableFuture)

Вы, наверное, уже слышали о Future
Future представляет собой отложенный результат асинхронных вычислений. Он предоставляет метод – get, который возвращает результат вычисления, когда они завершены.
Проблема заключается в том, что вызов этого метода блокируется до тех пор, пока вычисление не будут сделаны до конца. Это весьма неудобно и может быстро сделать асинхронные вычисления бессмысленными
Конечно – вы можете создавать новые асинхронные задачи и передавать их executor-у, чтобы он выполнял ее в новом потоке. Но почему вы должны беспокоиться обо всем этом, когда должны думать только о логике своей программы.

Это где CompletableFuture вступает в игру
Помимо реализации интерфейса Future, CompletableFuture также реализует интерфейс CompletionStage.
CompletionStage это аналог Promise в JavaScript. Он обещает, что вычисление в конечном итоге будет сделано.
Самое замечательное то, что CompletionStage предлагает большой набор методов, которые позволяют прикреплять колбеки, которые будут выполняться по завершении асинхронной задачи.
Таким образом, мы можем строить системы в неблокирующем стиле, например как сервер на Node.js.

Простейшие асинхронное вычисление
Давайте начнем с самого элементарного – создадим простую асинхронную задачу – отправление сообщения.

CompletableFuture.supplyAsync(this::sendMsg);

supplyAsync принимает функцию Supplier, содержащий код, который мы хотим выполнить асинхронно — в нашем случае метод sendMsg.
Если вы работали немного с Future-ми в прошлом, вы можете задаться вопросом, где же Executor? Если вы хотите, вы можете передать его в качестве второго аргумента метода supplyAsync. Тем не менее, если вы его не укажите, то по умолчанию будет использован ForkJoinPool.commonPool ().

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

CompletableFuture.supplyAsync(this::sendMsg)
.thenAccept(this::notify);

thenAccept является одним из многих способов добавить колбек функцию. Он принимает функцию Consumer – в нашем случае метод notify, который обрабатывает результат предыдущего вычисления, после его завершения.

Последовательный вызов нескольких колбеков
Если вы хотите продолжить передачу значений от одного колбека к другому, thenAccept не позволит этого сделать, поскольку функция Consumer не возвращает ничего.
Чтобы сохранить передачу значений, вы можете просто использовать thenApply вместо thenAccept .
thenApply принимает функцию, которая принимает значение, но и возвращать что-то в ответ.
Чтобы увидеть, как это работает, давайте расширим наш предыдущий пример, сначала найдя получателя сообщения.

CompletableFuture.supplyAsync(this::findReceiver)
.thenApply(this::sendMsg)
.thenAccept(this::notify);

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

Построение асинхронной программы
При построении больших асинхронных программ, все работает немного по-другому.
Обычно мы хотим, чтобы создание новых частей кода было основано на более мелких кусочках. Каждый из этих кусочков, как правило, должен быть асинхронным – в нашем случае возвращать CompletionStage.
До сих пор, sendMsg было нормальной блокирующей функцией. Давайте теперь предположим, что мы получили метод sendMsgAsync, который возвращает CompletionStage.
Если мы продолжим использовать thenApply, мы бы столкнулись с вложенными CompletionStage.

CompletableFuture.supplyAsync(this::findReceiver)
.thenApply(this::sendMsgAsync);

// Возвращаемый тип CompletionStage >

Мы не хотим этого, так что вместо этого мы можем использовать thenCompose, который позволяет сделать функцию, которая возвращает CompletionStage. Работа этого метода похожа на flatMap.

CompletableFuture.supplyAsync(this::findReceiver)
.thenCompose(this::sendMsgAsync);

// Возвращаемый тип CompletionStage

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

CompletableFuture.supplyAsync(this:: sendMsgWithFail)
.exceptionally(ex -> new MsgFail())
.thenAccept(this::notify);

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

Колбек зависит от нескольких асинхронных задач
Иногда это было бы очень полезно, чтобы иметь возможность создать функцию колбек, которая зависит от результата двух вычислений. Для этого нужно использовать thenCombine.
thenCombine позволяет зарегистрировать колбек типа BiFunction в соответствии от результата двух CompletionStage.
Чтобы увидеть, как это делается, давайте в дополнение к поиску получателя также выполнить тяжелую работу по созданию некоторого содержимого сообщения перед его отправкой.

CompletableFuture receiver =
CompletableFuture.supplyAsync(this::findReceiver);

CompletableFuture content =
CompletableFuture.supplyAsync(this::createContent);

receiver.thenCombine(content, this::sendMsg);

Сначала мы запустили две асинхронных задачи – нахождение получателя и создания некоторого содержимого. Затем мы используем thenCombine, чтобы сказать, что мы хотим сделать с результатом этих двух вычислений путем определения нашей би-функции.
Стоит отметить, что существует еще один вариант thenCombine, который называется runAfterBoth. Эта версия принимает Runnable не заботясь о фактических значениях предыдущих вычислений, методу важно только то, что эти вычисления закончены и можно выполнить колбек функцию.

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

CompletableFuture source1 =
CompletableFuture.supplyAsync(this::findFirstSource);

CompletableFuture source2 =
CompletableFuture.supplyAsync(this::findSecondSource);

source1.acceptEither(source2, this::sendMsg);

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

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

Часть 2. Выполнение задач в многопоточном режиме

Серия контента:

Этот контент является частью # из серии # статей: Вселенная Java

Этот контент является частью серии: Вселенная Java


Следите за выходом новых статей этой серии.

При разработке ПО часто возникают ситуации, когда необходимо обеспечить одновременное выполнение нескольких задач, например, отправку и получение электронной почты. В корпоративных JEE-приложениях для этого предлагается использовать специальные инфраструктуры и библиотеки, такие как Quartz. Но для JSE-приложений такой подход может не окупиться, так как для внедрения инфраструктуры ресурсов потребуется больше, чем для реализации самих задач.

Поэтому в данной статье рассматриваются два подхода к параллельному выполнению задач в JSE-приложениях: классический способ и новые возможности пакета java.util.concurrent.

Классический подход к запуску задач в многопоточном режиме

Классический подход к запуску задач в многопоточном режиме в JSE предполагает использование класса java.lang.Thread или интерфейса java.lang.Runnable. В первом случае программист создает потомка класса Thread и переопределяет в нем метод run, куда помещается функциональность, которую необходимо выполнить в многопоточном режиме, как показано в листинге 1.

Листинг 1. Запуск задач с помощью класса java.lang.Thread

Использование интерфейса Runnable основывается на другой парадигме. Сначала программист должен реализовать интерфейс Runnable в собственном классе, а затем поместить объект этого класса в объект типа Thread. Интересующая функциональность также помещается в метод run класса, реализующего интерфейс Runnable, и впоследствии вызывается объектом-контейнером, как показано в листинге 2.

Листинг 2. Запуск задач с помощью интерфейса java.lang.Runnable

Как видно в обоих примерах запуск задачи в многопоточном режиме выполняется через вызов метода start объекта типа Thread. Только в первом случае после вызова метода start класса Thread происходит вызов метода run, наследника этого класса (класса ThreadSample), в котором и находится код, относящейся к задаче. При выборе реализации на основе интерфейса Runnable сначала происходит вызов метода start класса Thread, затем обращение к методу run этого же класса, и уже из этого метода вызывается метод run реализации интерфейса Runnable (класса RunnableSample).

Ошибки при использовании классического подхода

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

Во-первых, часто вместо вызова метода start для запуска потока программист сразу вызывает метод run, что приводит к неправильному результату. Если сразу вызвать метод run на строке 8 в листинге 1 или листинге 2, то программа отработает без всяких видимых изменений. Ошибка заключается в том, что при непосредственном вызове метода run задача будет выполняться, но в однопоточном, а не в многопоточном режиме. Поэтому, если такая задача всего одна, программа отработает нормально, хотя и несколько медленнее (но «невооруженным» глазом это будет незаметно), а вот в случае с несколькими задачами падение производительности окажется фатальным.

Другая проблема связана с самим использованием наследования класса Thread вместо реализации интерфейса Runnable. Если при реализации интерфейса требуется обязательное соблюдение сигнатуры при переопределении метода (в данном случае метода run), то в наследовании такого ограничения не существует. Поэтому ошибка в сигнатуре метода run в листинге 1 автоматически меняет состояние этого метода с «переопределенный» на «перегруженный», при этом при компиляции не будет выведено никаких предупреждений или сообщений об ошибках. Однако запуск подобной программы опять приведет к возникновению непредусмотренного результата, а точнее, полному отсутствию такового. Это будет связано с тем, что при отсутствии переопределенной версии метода run будет вызвана реализация этого метода по умолчанию, которая расположена в классе Thread. Эта реализация по умолчанию не содержит никакой функциональности, соответственно поток запустится и тут же остановится, так как никакой работы для него нет.

Для решения этой проблемы на помощь приходит одна из возможностей, появившихся в JSE 5, — аннотация Override, как показано на строке 8 в листинге 1. Эта аннотация заставляет компилятор выполнить проверку, действительно ли объявленный метод переопределяет какой-нибудь из методов суперкласса. В случае, если метод, отмеченный этой аннотацией, не является переопределением метода из суперкласса, то компилятор выводит сообщение об ошибке. Вообще, аннотацию Override рекомендуется применять во всех случаях, когда выполняется переопределение методов, так как это повышает качество разрабатываемого кода.

Выбор между интерфейсом java.lang.Runnable и классом java.lang.Thread

Как было показано ранее, при необходимости обеспечить параллельное выполнение нескольких задач у программиста есть возможность выбрать, как именно реализовать эти задачи: с помощью класса Thread или интерфейса Runnable. У каждого подхода есть свои преимущества и недостатки.

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

Использование интерфейса Runnable по умолчанию лишено этого недостатка, но если реализовать задачу таким способом, то придется потратить дополнительные усилия на ее запуск. Как было показано в листинге 2, для запуска Runnable-задачи все равно потребуется объект Thread, также в этом случае исчезнет возможность прямого управления потоком из задачи. Хотя последнее ограничение можно обойти с помощью статических методов класса Thread (например, метод currentThread() возвращает ссылку на текущий поток).

Поэтому сделать однозначный вывод о превосходстве какого-либо подхода довольно сложно, и чаще всего в приложениях одновременно используются оба варианта, но для решения задач различной направленности. Считается, что наследование класса Thread следует применять только тогда, когда действительно необходимо создать «новый вид потока, который должен дополнить функциональность класса java.lang.Thread», и подобное решение применяется при разработке системного ПО, например, серверов приложений или инфраструктур. Использование интерфейса Runnable показано в случаях, когда просто «необходимо одновременно выполнить несколько задач» и не требуется вносить изменений в сам механизм многопоточности, поэтому в бизнес-ориентированных приложениях в основном используется вариант с интерфейсом Runnable.

Ограничения классического подхода

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

Первым, что бросается в глаза, оказывается слияние низкоуровневого кода, отвечающего за многопоточное исполнение, и высокоуровневого кода, отвечающего за основную функциональность приложения (так называемый «спагетти-код»). В листинге 1 показано, что бизнес—код и поточный код вообще находятся в одном классе, но даже в более удачном варианте из листинга 2 для выполнения задачи все равно требуется создать объект Thread и запустить его. Подобное перемешивание снижает качество архитектуры приложения и может затруднить его последующее сопровождение.

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

  • поток занимает относительно много места в куче, так что после его завершения необходимо проследить, чтобы память, занимаемая им, была освобождена (например, присвоить ссылке на поток значение null);
  • для выполнения новой задачи потребуется запустить новый поток, что приведет к увеличению «накладных расходов» на виртуальную машину, так как запуск потока – это одна из самых требовательных к ресурсам операций.

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

Важно сказать, что «среднестатистический» программист будет отнюдь не первым, кто сталкивается с подобными проблемами в многозадачных приложениях. Все эти проблемы были давно проанализированы Java-сообществом и нашли решение в признанных шаблонах проектирования (например, ThreadPool (пул потоков) и WorkerThread (рабочий поток)). Но скорее всего, у рядового программиста, ограниченного временными рамками проекта, просто не будет времени или ресурсов, чтобы подготовить и самое главное протестировать полноценную реализацию данных шаблонов. А неточное или неполное внедрение этих шаблонов (да и вообще любых шаблонов проектирования) может в будущем негативно сказаться на этапе сопровождения продукта.

Новые возможности пакета java.uti.concurrent

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

В рамках этой статьи интерес представляют уже готовые к использованию реализации шаблонов WorkerThread и ThreadPool, а также еще один способ реализации задач для параллельного выполнения, кроме упоминавшихся класса Thread и интерфейса Runnable. Ещё в пакете java.util.concurrent находятся два подпакета: java.util.concurrent.locks и java.util.concurrent.atomic, с которыми тоже стоит ознакомиться, так как они значительно упрощают организацию взаимодействия между потоками и параллельного доступа к данным.

Создание задачи с помощью интерфейса java.util.concurrent.Callable

Интерфейс Callable гораздо больше подходит для создания задач, предназначенных для параллельного выполнения, нежели интерфейс Runnable или тем более класс Thread. При этом стоит отметить, что возможность добавить подобный интерфейс появилась только начиная с версии Java 5, так как ключевая особенность интерфейса Callable – это использование параметризованных типов (generics), как показано в листинге 3.

Листинг 3. Создание задачи с помощью интерфейса Callable

Сразу необходимо обратить внимание на строку 2, где указано, что интерфейс Callable является параметризованным, и его конкретная реализация – класс CallableSample, зависит от типа String. На строке 3 приведена сигнатура основного метода call в уже параметризованном варианте, так как в качестве типа возвращаемого значения также указан тип String. Фактически это означает, что была создана задача, результатом выполнения которой будет объект типа String (см. строку 8). Точно также можно создать задачу, в результате работы которой в методе call будет создаваться и возвращаться объект любого требуемого типа. Такое решение значительно удобнее по сравнению с методом run в интерфейсе Runnable, который не возвращает ничего (его возвращаемый тип – void) и поэтому приходится изобретать обходные пути, чтобы извлечь результат работы задачи.

Еще одно преимущество интерфейса Callable – это возможность «выбрасывать» исключительные ситуации, не оказывая влияния на другие выполняющиеся задачи. На строке 3 указано, что из метода может быть «выброшена» исключительная ситуация типа Exception, что фактически означает любую исключительную ситуацию, так как все исключения являются потомками java.lang.Exception. На строке 5 эта возможность используется для создания контролируемой (checked) исключительной ситуации типа IOException. Метод run интерфейса Runnable вообще не допускал выбрасывания контролируемых исключительных ситуаций, а выброс неконтролируемой (runtime) исключительной ситуации приводил к остановке потока и всего приложения.

Запуск задач с помощью java.util.concurrent.ExecutorService

Облегчив с помощью интерфейса Callable создание задач для параллельного выполнения, пакет java.util.concurrent также берет на себя работу по запуску и остановке потоков. Вместо объекта Thread предлагается использовать объект типа ExecutorService, с помощью которого пользователь может просто поместить задачу в очередь на выполнение и ждать получения результата. Можно сказать, что ExecutorService – это значительно усовершенствованная реализация шаблона WorkerThread.

ExecutorService – это интерфейс, поэтому для выполнения задач используются его конкретные потомки, адаптированные под требования разрабатываемого приложения. Однако программисту нет необходимости создавать собственную реализацию ExecutorService, так как в пакете java.util.concurrent уже присутствуют различные варианты реализации ExecutorService. Доступ к ним можно получить через статические методы служебного класса Executors, метод которого newFixedThreadPool возвращает объект типа ExecutorService со встроенной поддержкой шаблона ThreadPool. Также в классе Executors есть и другие методы для создания объектов ExecutorService с различными свойствами.

Наибольший интерес в ExecutorService представляет метод submit, через который задача ставится в очередь на выполнение. На вход этот метод принимает объект типа Callable или Runnable, а возвращает некий параметризованный объект типа Future. Этот объект можно использовать для доступа к результату выполнения задачи, который будет возвращен из метода call соответствующего Callable-объекта. При этом через объект Future можно проверить, закончено ли уже выполнение задачи – с помощью метода isDone и через метод get получить доступ к результату или исключительной ситуации, если в процессе выполнения задачи произошла ошибка.

Таким образом, при запуске задач с помощью классов из пакета java.util.concurrent не требуется прибегать к низкоуровневой поточной функциональности класса Thread, достаточно создать объект типа ExecutorService с нужными свойствами и передать ему на исполнение задачу типа Callable. Впоследствии можно легко просмотреть результат выполнения этой задачи с помощью объекта Future, как показано в листинге 4.

Листинг 4. Запуск задачи с помощью классов пакета java.util.concurrent

Стоит обратить внимание на строку 18, где происходит остановка объекта ExecutorService с помощью метода shutdown. Дело в том, что потоки в объекте ExecutorService не останавливаются сами, как обычно, поэтому их необходимо явно остановить с помощью этого метода, при этом если в ExecutorService находятся невыполненные задачи, то потоки будут остановлены только, когда завершится последняя задача.

Волшебный мир Java

воскресенье, 19 февраля 2012 г.

Многопоточность в Java. Часть 1

  • · реализацией интерфейса Runnable ;
  • · наследованием класса Thread .

  • Thread . MIN _ PRIORITY – равняется самому низкому приоритету 1;
  • Thread . NORM _ PRIORITY – равняется среднему приоритету 5 (данный приоритет задан по умолчанию);
  • Thread . MAX _ PRIORITY – равняется самому высокому приоритету 10.

  • join () – ожидает пока вызываемый поток не завершит свою реализации;
  • join ( long milis ) – ожидает завершения вызываемого потока указанное время, после чего передает управление вызывающему потоку.

26 комментариев:

спасбо, прочитал перед сном, ещё раз вспомнил основы.

Не за что. Очень рад, что статья пригодилась.

спасибо, не хватает только про wait и notify рассказать

Рад, что статья помогает.
Постараюсь в ближайшее время написать вторую часть статьи. Все никак руки не доходят. Но постараюсь исправиться :)

Недавно начал изучать java. Многопоточность по Эккелю понимается гораздо труднее, а благодаря вашей статье разобрался. Большое спасибо! :))

Очень приятно это слышать. Значит нужно продолжать начатое дело. Постараюсь, более шире раскрыть данную тему.

Изучаю джава по Шилдту 8 издание, не мог норм понять многопотоки, спасибо вам, жду с нетерпением продолжения «синхронизации потоков и пакет concurrent.»

И еще вот никак не могу разобраться, почему после наследования Thread не надо обращаться к потоку напрямую(как ето делается Runnable) вот два разных вызова «ob1.t.join» — runnable, «ob1.join» наследование. Я понимаю что ето изза наследования, но вот конретно изза чего не могу понять.. Уже перечитывал раздел Наследование, но толку ноль. Клас как бы наследует Thread и все его переменные екземпляра, типа как «t». Но вот почему не надо на нее ссылаться, во время наследования остается для меня загадкой. Спасибо за любую помочь.

Шилдт 8 издание — неплохая книга, но как по мне больше похожа на справочник. Хотя, каждому свой подход и изложение подходит.
Как вариант можно обратить внимание на книги:
— Java 2. Том 1. Основы. 8-е издание — Хорстманн, Корнелл
— Java 2. Том 2. Тонкости программирования. 8-е издание — Хорстманн, Корнелл
— Философия Java. Библиотека программиста. 4-е издание — Брюс Эккель.

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

То есть в первом случае obj1 — это экземпляр класса содержащего экземпляр потока, а во втором obj1 — это экземпляр класса расширяющего поток.

Надеюсь получилось донести мысль сказанного :).

Thinking In Java Enterprise (русский перевод) → Сетевое программирование с Сокетами и Каналами

Одна из сильных сторон Java заключается в безпроблеммной сетевой работе. Дизайнеры сетевой библиотеки Java сделали ее достаточно простой для чтения и записи файлов, за исключением случая, когда «файл» существует на удаленной машине и удаленная машина может решать что ей делать с информацией, которую вы запрашиваете или посылаете. Насколько это возможно, низлежащие детали сетевого взаимодействия были абстрагированы и о них заботится ядро JVM и локальный пакет установки Java. Программная модель, которую вы используете для такого файла, фактически, это обертка сетевого соединения («сокет») с объектом потока, так что в конечном счете вы используете те же вызовы методов, которые вы используете для других потоков. Кроме того, встроенная многопоточность Java исключительно удобна, когда вы имеете дело с такой сетевой возможностью, как обработка множества соединений одновременно.

Этот раздел является вводным в сетевое взаимедействие Java с использованием легких в понимании примеров.

Идентификация машины

Конечно, для того, чтобы передать данные с одной машины на другую необходимо убедиться, что вы подсоединились к определенной машине в сети. Ранние варианты сетей были удовлетворены предоставлением уникальных имен машинам внутри локальной сети. Однако, Java работает в пределах Internet, что требует способа для уникальной идентификации машины из любой точки всего мира. Это выполняется с помощью IP (Internet Protocol) адреса, который может существовать в двух формах:

  1. Привычная форма DNS (Domain Name System). Мое доменное имя — bruceeckel.com, и если у меня есть компьютер, называемый Opus в моем домене, его доменное имя должно быть Opus.bruceeckel.com. Это в точности имя такого рода, которое вы используете при отсылке электронной почты людям, и часто он встроен в адрес World Wide Web.
  2. Альтернативный вариант: вы можете использовать форму из четырех чисел, разделенных точками, например 123.255.28.120.


В обоих случаях IP адрес представляется как 32-х битное число [1] (так как каждое из четырех чисел не может превышать 255), и вы можете получить специальный Java объект для представления этого числа из любой из перечисленных выше форм, используя статический метод InetAddress.getByName( ), который определен в java.net. Результатом будет объект типа InetAddress, который вы можете использовать для создания «сокета», как вы это увидите далее.

В качестве простейшего примера использования InetAddress.getByName() рассмотрим, что произойдет при использовании коммутируемого доступа (dial-up Internet service provider (ISP)). При каждом дозвоне вам назначается временный IP адрес. Но пока вы соединены, ваш IP адрес имеет такую же силу, как и другие IP адреса в Internet. Если кто-либо соединится с вашей машиной использую ваш IP адрес, то он может соединится с Web сервером или FTP сервером, который запущен на вашей машине. Конечно, ему необходимо знать ваш IP адрес, а так как при каждом дозвоне вам назначается новый адрес, то как вы можете определеть какой у вас адрес?

Приведенная ниже программа использует InetAddress.getByName( ) для воспроизведения вашего IP адреса. Для ее использования вы должны знать имя вашего компьютера. Под управлением Windows 95/98 перейдите в «Settings», «Control Panel», «Network» и выберите закладку «Identification». Содержимое в поле «Computer name» является той строкой, которую необходимо поместить в командную строку.

//: c15:WhoAmI.java
// Нахождение вашего сетевого адреса, когда
// вы соединены с Internet’ом.
// <Запускается руками>Должно быть установлено соединение с Internet
//
import java.net.*;

public class WhoAmI <
public static void main ( String [] args ) throws Exception <
if ( args.length != 1 ) <
System.err.println ( «Usage: WhoAmI MachineName» ) ;
System.exit ( 1 ) ;
>
InetAddress a = InetAddress.getByName ( args [ 0 ]) ;
System.out.println ( a ) ;
>
> // /:

В моем случае, машина называется «peppy». Так что, когда я соединюсь с моим провайдером и запущу программу:

Я получу назад сообщение такого типа (конечно же, адрес отличается при каждом новом соединении):

Если я скажу этот адрес моему другу и у меня будет запущен Web Сервер на моем компьютере, он сможет соединится с сервером, перейдя по ссылке http://199.190.87.75 (только до тех пор, пока я остаюсь соединенным во время одной сессии). Иногда это может быть ручным способом распределения информации кому-то еще или использоваться для тестирования конфигурации Web сайта перед размещением его на «реальном» сервере.

Серверы и клиенты

Основное назначение сети состоит в том, чтобы позволить двум машинам соединиться и пообщаться друг с другом. Так как две машины могут найти друг друга, они могут провести милую, двусторонню беседу. Но как они могут найти друг друга? Это похоже на поиск потерянных в парке аттракционов: одна машина стоит в одном месте и слушает, пока другая машина скажет: «Эй, ты где?».

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

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

Тестирование программ без сети

По многим причинам, вы можете не иметь клиентской машины, серверной машины и сети, доступных для тестирования ваших программ. Вы можете выполнять упражнения в обстановке классной комнаты, или, возможно, вы пишите программы, которые еще не достаточно стабильны и не могут быть выложены в сеть. Создатели Internet Protocol учли эту возможность и создали специальный адрес, называемый localhost, IP адрес «локальной заглушки (local loopback)» для тестирования без использования сети. Общий способ для получения такого адреса в Java такой:

Если вы передадите в getByName( ) значение null, метод по умолчанию будет использовать localhost. InetAddress является тем, что вы используете для указания определенной машины, и вы должны произвести его прежде, чем вы можете двинуться далее. Вы не можете манипулировать содержимым InetAddress (но вы можете напечатать его, как это будет показано в следующем примере). Единственный способ, которым вы можете создать InetArddress, это через один из перегруженных статических методов класса getByName( ) (который является тем, что вы уже использовали), getAllByName(), или getLocalHost( ).

Вы также можете получить адрес локальной заглушки, передав строку localhost:

(предполагается, что «localhost» сконфигурирован в таблице «hosts» на вашей машине), или используя цифровую четырехзначную форму для имени, представляющем заглушку:

Все три формы произовдят одинаковый результат.

Порт: уникальное место внутри машины

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

Порт — это не физическое расположение в машине, а программная абстракция (в основном для целей учета). Клиентская программа знает, как соединится к машине через ее IP адрес, но как она может присоединится к определенной службе (потенциально, к одной из многих на этой машине)? Таким образом номер порта стал вторым уровнем адресации. Идея состоит в том, что при запросе определенного порта вы запрашиваете службу, ассоциированную с этим номером порта. Служба времени — простейший пример службы. Обычно каждая служба ассоциируется с уникальным номером порта на определенной серверной машине. Клиент должен предварительно знать, на каком порту запущена нужная ему служба.

Системные службы зарезервировали использование портов с номерам от 1 до 1024, так что вы не можете использовать этот или любой другой порт, про который вы знаете, что он задействован. Первым выбором, например, в этой книге будет порт 8080 (в память многоуважаемого 8-битного процессора 8080 от Intel в моем первом компьютере, CP/M машине).

Сокеты

Сокет — это программная абстракция, используемая для представления «терминалов» соединения между двумя машинами. Для данного соединения есть сокет на каждой машине, и вы можете представить гипотетический «кабель», включенный в сокет. Конечно, физическое оборудование и каблирование между машинами полностью неизвестно. Главное назначение абстракции состоит в том, что мы не должны знать более того, что нам необходимо.

В Java вы создаете сокет, чтобы создать соединение с другой машиной, затем вы получаете InputStream и OutputStream (или, с соответствующими конверторами, Reader и Writer) из сокета, чтобы получить возможность трактовать соединение, как объект потока ввода/вывода. Существует два класса сокетов, основанных на потоках: ServerSocket, который использует сервер для «прослушивания» входящих соединения, и Socket, который использует клиент для инициализации соединения. Как только клиент создаст сокетное соединение, ServerSocket возвратит (посредством метода accept( )) соответствующий Socket, через который может происходить коммуникация на стороне сервера. После этого вы общаетесь в соединении через Socket с Socket’ом и вы трактуете оба конца одинаково, посколько они и являются одним и тем же. На этой стадии вы используете методы getInputStream( ) и getOutputStream( ) для получения соответствующих объектов InputStream’а и outputStream’а для каждого сокета. Они должны быть обернуты внутрь буферных и форматирующих классов точно так же, как и другие объекты потоков, описанные в Главе 11.

Использование термина ServerSocket может показаться другим примером сбивающей с толку схемы именования в библиотеках Java. Вы можете подумать, что ServerSocket лучше было бы назвать «ServerConnector» или как-то подругому, без слова «Socket» внутри. Вы также можете подумать, что ServerSocket и Socket должны оба наследоваться от какого-то общего базового класса. На самом деле, два калсса имеют некоторые общие методы, но не настолько, чтобы дать им общий базовый класс. Вместо этого, работа ServerSocket’а состоит в том, чтобы ждать, пока некоторая машина не присоединится к нему, а затем он возвращает реальный Socket. Вот почему кажется, что ServerSocket назван немножко неправильно, так как его работа состоит не в том, чтобы быть реальным сокетом, а в том, чтобы создавать объект Socket’а, когда кто-то присоединяется к нему.

Однако, ServerSocket создает физический «сервер» или слушающий сокет на хост-машине. Этот сокет слушает входящие соединения, а затем возвращает «связанный» сокет (с определенными локальной и удаленной конечными точками) посредством метода accept( ). Сбивающая часть состоит в том, что оба эти сокета (слушающий и связанный) ассоциированы с одним и тем же серверным сокетом. Слушающий сокет может принять только новый запрос на соединение, а не пакет данных. Так что, не смотря на то, что ServerSocket имеет мало смыла с точки зрения программирования, в нем много смысла «физически».

Когда вы создаете ServerSocket, вы даете ему только номер порта. Вы не даете ему IP адрес, поскольку он уже есть на той машине, на которой он представлен. Однако когда вы создаете Socket, вы должны передать ему и IP адрес, и номер порта, к которому вы хотите присоединиться. (Однако Socket, который возвращается из метода ServerSocket.accept( ) уже содержит всю эту информацию.)

Простейший сервер и клиент

Этот пример покажет простейшее использование серверного и клиентского сокета. Все, что делает сервер, это ожидает соединения, затем использует сокет, полученный при соединении, для создания InputStream’а и OutputStream’а. Они конвертируются в Reader и Writer, которые оборачиваются в BufferedReader и PrintWriter. После этого все, что будет прочитано из BufferedReader’а будет переправлено в PrintWriter, пока не будет получена строка «END», означающая, что пришло время закрыть соединение.

Клиент создает соединение с сервером, затем создает OutputStream и создает некоторую обертку, как и в сервере. Строки текста посылаются через полученный PrintWriter. Клиент также создает InputStream (опять таки, с соответствующей конвертацией и оберткой), чтобы слушать, что говорит сервер (который, в данном случае, просто отсылает слова назад).

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

//: c15:JabberServer.java
// Очень простой сервер, который просто отсылает
// назад все, что посылает клиент.
//
import java.io.*;

public class JabberServer <
// Выбираем порт вне пределов 1-1024:
public static final int PORT = 8080 ;

public static void main ( String [] args ) throws IOException <
ServerSocket s = new ServerSocket ( PORT ) ;
System.out.println ( «Started: » + s ) ;
try <
// Блокирует до тех пор, пока не возникнет соединение:
Socket socket = s.accept () ;
try <
System.out.println ( «Connection accepted: » + socket ) ;
BufferedReader in = new BufferedReader ( new InputStreamReader (
socket.getInputStream ())) ;
// Вывод автоматически выталкивается из буфера PrintWriter’ом
PrintWriter out = new PrintWriter ( new BufferedWriter (
new OutputStreamWriter ( socket.getOutputStream ())) , true ) ;
while ( true ) <
String str = in.readLine () ;
if ( str.equals ( «END» ))
break ;
System.out.println ( «Echoing: » + str ) ;
out.println ( str ) ;
>
// Всегда закрываем два сокета.
>
finally <
System.out.println ( «closing. » ) ;
socket.close () ;
>
>
finally <
s.close () ;
>
>
> // /:

Вы можете видеть, что для ServerSocket’а необходим только номер порта, а не IP адрес (так как он запускается на локальной машине!). Когда вы вызываете accept( ), метод блокирует выполнение до тех пор, пока клиент не попробует подсоединится к серверу. То есть, сервер ожидает соединения, но другой процесс может выполнятся (смотрите Главу 14). Когда соединение установлено, метод accept( ) возвращает объект Socket, представляющий это соединение.

Здесь тщательно обработана отвественность за очистку сокета. Если конструктор ServerSocket завершится неудачей, программа просто звершится (обратите внимание, что мы должны предположить, что конструктор ServerSocket не оставляет никаких открытых сокетов, если он зваершается неудачей). По этой причине main( ) выбрасывает IOException, так что в блоке try нет необходимости. Если конструктор ServerSocket завершится успешно, то все вызовы методов должны быть помещены в блок try-finally, чтобы убедиться, что блок не будет покинут ни при каких условиях и ServerSocket будет правильно закрыт.

Аналогичная логика используется для сокета, возвращаемого из метода accept( ). Если метод accept( ) завершится неудачей, то мы должны предположить, что сокет не существует и не удерживает никаких ресурсов, так что он не нуждается в очистке. Однако если он закончится успешно, то следующие выражения должны быть помещены в блок try-finally, чтобы при каких-либо ошибках все равно произошла очистка. Позаботится об этом необходимо, потому что сокеты используют важные ресурсы, не относящиеся к памяти, так что вы должны быть прилежны и очищать их (так как в Java нет деструкторов, чтобы сделать это за вас).

И ServerSocket и Socket, производимый методом accept( ), печатаются в System.out. Это означает, что автоматически вызывается их метод toString( ). Вот что он выдаст:

Короче говоря, вы увидите как это соответствует тому, что делает клиент.

Следующая часть программы выглядит, как открытие файла для чтения и записи за исключением того, что InputStream и OutputStream создаются из объекта Socket. И объект InputStream’а и OutputStream’а конвертируются в объекты Reader’а и Writer’а с помощью «классов-конвертеров» InputStreamReader и OutputStreamreader, соответственно. Вы можете также использовать классы из Java 1.0 InputStream и OutoutStream напрямую, но, с точки зрения вывода, есть явное преимущество в использовании этого подхода. Оно проявляется в PrintWriter’е, который имеет перегруженный конструктор, принимающий в качестве второго аргумента флаг типа boolean, указывающий, нужно ли автоматическое выталкивание буфера вывода в конце каждого выражения println( ) (но не print( )). Каждый раз, когда вы записываете в вывод, буфер вывода должен выталкиваться, чтобы информация проходила по сети. Выталкивание важно для этого конкретного примера, поскольку клиент и сервер ожидают строку от другой стороны, прежде, чем приступят к ее обработке. Если выталкивание буфера не произойдет, информация не будет помещена в сеть до тех пор, пока буфер не заполнится, что может привести к многочисленным проблемам в этом примере.

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

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

В бесконечном цикле while происходит чтение строк из входного BufferedReader’а и запись информации в System.out и в выходной PrintWriter. Обратите внимание, что вход и выход могут быть любыми потоками, так случилось, что они связаны с сетью.

Когда клиент посылает строку, содержащую «END», программа прекращает цикл и закрывает сокет.

//: c15:JabberClient.java
// Очень простой клиент, который просто посылает
// строки на сервер и читает строки,
// посылаемые сервером.
//
import java.net.*;

public class JabberClient <
public static void main ( String [] args ) throws IOException <
// Передаем null в getByName(), получая
// специальный IP адрес «локальной заглушки»
// для тестирования на машине без сети:
InetAddress addr = InetAddress.getByName ( null ) ;
// Альтернативно, вы можете использовать
// адрес или имя:
// InetAddress addr =
// InetAddress.getByName(«127.0.0.1»);
// InetAddress addr =
// InetAddress.getByName(«localhost»);
System.out.println ( «addr = » + addr ) ;
Socket socket = new Socket ( addr, JabberServer.PORT ) ;
// Помещаем все в блок try-finally, чтобы
// быть уверенным, что сокет закроется:
try <
System.out.println ( «socket = » + socket ) ;
BufferedReader in = new BufferedReader ( new InputStreamReader ( socket
.getInputStream ())) ;
// Вывод автоматически Output быталкивается PrintWriter’ом.
PrintWriter out = new PrintWriter ( new BufferedWriter (
new OutputStreamWriter ( socket.getOutputStream ())) , true ) ;
for ( int i = 0 ; i 10 ; i++ ) <
out.println ( «howdy » + i ) ;
String str = in.readLine () ;
System.out.println ( str ) ;
>
out.println ( «END» ) ;
>
finally <
System.out.println ( «closing. » ) ;
socket.close () ;
>
>
> // /:

В main( ) вы можете видеть все три способа получение InetAddress IP адреса локальной заглушки: с помощью null, localhost или путем явного указания зарезервированного адреса 127.0.0.1, если вы хотите соединится с машиной по сети, вы замените это IP адресом машины. Когда печатается InetAddress (с помощью автоматического вызова метода toString( )), то получается результат:

При передачи в getByName( ) значения null, он по умолчанию ищет localhos и затем производит специальныйы адрес 127.0.0.1.

Обратите внимание, что Socket создается при указании и InetAddress’а, и номера порта. Чтобы понять, что это значит, когда будете печатать один из объектов Socket помните, что Интернет соединение уникально определяется четырьмя параметрами: клиентским хостом, клиентским номером порта, серверным хостом и серверным номером порта. Когда запускается сервер, он получает назначаемый порт (8080) на localhost (127.0.0.1). Когда запускается клиент, он располагается на следующем доступном порту на своей машине, 1077 — в данном случае, который так же оказался на той же самой машине (127.0.0.1), что и сервер. Теперь, чтобы передать данные между клиентом и сервером, каждая сторона знает, куда посылать их. Поэтому, в процессе соединения с «известным» сервером клиент посылает «обратный адрес», чтобы сервер знал, куда посылать данные. Вот что вы видите среди выводимого стороной сервера:

Это означает, что сервер просто принимает соединение с адреса 127.0.0.1 и порта 1077 во время прослушивания локального порта (8080). На клиентской стороне:

Это значит, что клиент установил соединение с адресом 127.0.0.1 по порту 8080, используя локальный порт 1077.

Вы заметите, что при каждом повторном запуске клиента номер локального порта увеличивается. Он начинается с 1025 (первый после зарезервированного блока портов) и будет увеличиваться до тех пор, пока вы не перезапустите машину, в таком случае он снова начнется с 1025. (На машинах под управлением UNIX, как только будет достигнут верхний предел диапазона сокетов, номер будет возвращен снова к наименьшему доступному номеру.)

Как только объект Socket будет создан, процесс перейдет к BufferedReader и PrintWriter, как мы это уже видели в сервере (опять таки, в обоих случаях вы начинаете с Socket’а). В данном случае, клиент инициирует обмен путем посылки строки «howdy», за которой следует число. Обратите внимание, что буфер должен опять выталкиваться (что происходит автоматически из-за второго аргумента в конструкторе PrintWriter’а). Если буфер не будет выталкиваться, процесс обмена повиснет, поскольку начальное «howdy» никогда не будет послана (буфер недостаточно заполнен, чтобы отсылка произошла автоматически). Каждая строка, посылаемая назад сервером, записывается в System.out, чтобы проверить, что все работает корректно. Для завершения обмена посылается ранее оговоренный «END». Если клиент просто разорвет соединение, то сервер выбросит исключение.

Вы можете видеть, что аналогичные меры приняты, чтобы быть уверенным в том, что сетевые ресурсы, представляемые сокетом, будут правильно очищены. Для этого используется блок try-finally.

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

Обслуживание множества клиентов

JabberServer работает, но он может обработать только одного клиента одновременно. В обычных серверах вы захотите, чтобы была возможность иметь дело со многими клиентами одновременно. Ответом является многопоточность, и в языках, которые не поддерживают многопоточность напрямую, это означает что вы встретите все возможные трудности. В Главе 14 вы видели, что многопоточность в Java проста насколько это возможно, учитывая это, можно сказать, что многопоточность весьма сложная тема. Поскольку нити (потоки) в Java достаточно прамолинейны, то создание сервера, который обрабатывает несколько клиентов, относительно простое заняте.

Основная схема состоит в создании единственного ServerSocket’а на сервере и вызове метода accept( ) для ожидания новых соединений. Когда accept( ) возвращается, вы получаете результирующий сокет и используете его для создания новой нити (потока), работа которой будет состоять в ослуживании определенного клиента. Затем вы вызовите метод accept( ) снова, чтобы подождать нового клиента.

В следующем коде сервера вы можете видеть, что он очень похож на пример JabberServer.java, за исключением того, что все операции по обслуживанию определенного клиента былы помещены внутрь отдельного thread-класса:

//: c15:MultiJabberServer.java
// Сервер, который использует многопоточность
// для обработки любого числа клиентов.
//
import java.io.*;

class ServeOneJabber extends Thread <
private Socket socket;
private BufferedReader in;
private PrintWriter out;

public ServeOneJabber ( Socket s ) throws IOException <
socket = s;
in = new BufferedReader ( new InputStreamReader ( socket.getInputStream ())) ;
// Включаем автоматическое выталкивание:
out = new PrintWriter ( new BufferedWriter ( new OutputStreamWriter ( socket
.getOutputStream ())) , true ) ;
// Если любой из вышеприведенных вызовов приведет к
// возникновению исключения, то вызывающий отвечает за
// закрытие сокета. В противном случае, нить
// закроет его.
start () ; // вызываем run()
>

public void run () <
try <
while ( true ) <
String str = in.readLine () ;
if ( str.equals ( «END» ))
break ;
System.out.println ( «Echoing: » + str ) ;
out.println ( str ) ;
>
System.out.println ( «closing. » ) ;
>
catch ( IOException e ) <
System.err.println ( «IO Exception» ) ;
>
finally <
try <
socket.close () ;
>
catch ( IOException e ) <
System.err.println ( «Socket not closed» ) ;
>
>
>
>

public class MultiJabberServer <
static final int PORT = 8080 ;

public static void main ( String [] args ) throws IOException <
ServerSocket s = new ServerSocket ( PORT ) ;
System.out.println ( «Server Started» ) ;
try <
while ( true ) <
// Блокируется до возникновения нового соединения:
Socket socket = s.accept () ;
try <
new ServeOneJabber ( socket ) ;
>
catch ( IOException e ) <
// Если завершится неудачей, закрывается сокет,
// в противном случае, нить закроет его:
socket.close () ;
>
>
>
finally <
s.close () ;
>
>
> // /:

Нить ServeOneJabber принимает объект Socket’а, который производится методом accept( ) в main( ) при каждом новом соединении с клиентом. Затем, как и прежде, с помощью Socket, создается BufferedReader и PrintWriter с возможностью автоматического выталкивания буфера. И наконец, вызывается специальный метод нити start( ). Здесь выполняются те же действия, что и в предыдущем примере: читается что-то из сокета и затем отсылается обратно до тех пор, пока не будет прочитан специальный сигнал «END».

Ответственность за очистку сокета должна быть, опять таки, внимательно спланирована. В этом случае, сокет создается вне ServeOneJabber, так что ответственность может быть совместная. Если конструктор ServeOneJabber завершится неудачей, он просто выбросит исключение тому, кто его вызвал, и кто должен очистить нить. Но если конструктор завершился успешно, то объект ServeOneJabber принимает ответственность за очистку нити на себя, в своем методе run( ).

Обратите внимание на упрощенность MultiJabberServer. Как и прежде создается ServerSocket и вызывается метод accept( ), чтобы позволить новое соединение. Но в это время возвращаемое значение метода accept( ) (сокет) передается в конструктор для ServeOneJabber, который создает новую нить для обработки этого соединения. Когда соединение завершиется, нить просто умирает.

Если создание ServerSocket’а проваливается, то из метода main( ), как и прежде, выбрасывается исключение. Но если создание завершается успешно, внешний блок try-finally гарантирует очистку. Внутренний try-catch гарантирует только от сбоев в конструкторе ServeOneJabber. Если конструктор завершится успешно, то нить ServeOneJabber закроет соответствующий сокет.

Для проверки этого сервера, который реально обрабатывает несколько клиентов, приведенная ниже программа создает несколько клиентов (используя нити), которые соединяются с одним и тем же сервером. Максимальное допустимое число нитей определяется переменной final int MAX_THREADS.

//: c15:MultiJabberClient.java
// Клиент, который проверяет MultiJabberServer,
// запуская несколько клиентов.
//
import java.net.*;

class JabberClientThread extends Thread <
private Socket socket;
private BufferedReader in;
private PrintWriter out;
private static int counter = 0 ;
private int > private static int threadcount = 0 ;

public static int threadCount () <
return threadcount;
>

public JabberClientThread ( InetAddress addr ) <
System.out.println ( «Making client » + id ) ;
threadcount++;
try <
socket = new Socket ( addr, MultiJabberServer.PORT ) ;
>
catch ( IOException e ) <
System.err.println ( «Socket failed» ) ;
// Если создание сокета провалилось,
// ничего ненужно чистить.
>
try <
in = new BufferedReader ( new InputStreamReader ( socket
.getInputStream ())) ;
// Включаем автоматическое выталкивание:
out = new PrintWriter ( new BufferedWriter ( new OutputStreamWriter (
socket.getOutputStream ())) , true ) ;
start () ;
>
catch ( IOException e ) <
// Сокет должен быть закрыт при любой
// ошибке, кроме ошибки конструктора сокета:
try <
socket.close () ;
>
catch ( IOException e2 ) <
System.err.println ( «Socket not closed» ) ;
>
>
// В противном случае сокет будет закрыт
// в методе run() нити.
>

public void run () <
try <
for ( int i = 0 ; i 25 ; i++ ) <
out.println ( «Client » + id + «: » + i ) ;
String str = in.readLine () ;
System.out.println ( str ) ;
>
out.println ( «END» ) ;
>
catch ( IOException e ) <
System.err.println ( «IO Exception» ) ;
>
finally <
// Всегда закрывает:
try <
socket.close () ;
>
catch ( IOException e ) <
System.err.println ( «Socket not closed» ) ;
>
threadcount—; // Завершаем эту нить
>
>
>

public class MultiJabberClient <
static final int MAX_THREADS = 40 ;

public static void main ( String [] args ) throws IOException,
InterruptedException <
InetAddress addr = InetAddress.getByName ( null ) ;
while ( true ) <
if ( JabberClientThread.threadCount () )
new JabberClientThread ( addr ) ;
Thread.currentThread () .sleep ( 100 ) ;
>
>
> // /:

Конструктор JabberClientThread принимает InetAddress и использует его для открытия сокета. Вероятно, вы заметили шаблон: сокет всегда используется для создания определенного рода объектов Reader’а и Writer’а (или InputStream и/или OutputStream), которые являются тем единственным путем, которым может быть использован сокет. (Вы можете, конечно, написать класс или два для автоматизации этого процесса вместо набора этого текста, если вас это беспокоит.) Далее, start( ) выполняет инициализацию нити и запуск run( ). Здесь сообщение посылается на сервер, а информация с сервера отображается на экране. Однако, нить имеет ограниченноен время жизни и, в конечном счете, завершается. Обратите внимание, что сокет очищается, если конструктор завершился неудачей после создания сокета, но перед тем, как конструктор завершится. В противном случае, ответственность за вызов close( ) для сокета ложиться на метод run( ).

Threadcount хранит информацию о том, сколько в настоящее время существует объектов JabberClientThread. Эта переменная инкрементируется, как часть конструктора и декрементируется при выходе из метода run( ) (что означает, что нить умерла). В методе MultiJabberClient.main( ) вы можете видеть, что количество нитей проверяется, и если их много, то нить более не создается. Затем метод засыпает. Таким образом, некоторые нити, в конечном счете, умрут, и другие будут созданы. Вы можете поэкспериментировать с MAX_THREADS, чтобы увидеть, когда ваша конкретная система почувствует затруднения со множеством соединений.

Дейтаграммы

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

Существует второй потокол, называемый User Datagram Protocol (UDP), который не гарантирует, что пакет будет доставлен и не гарантирует, что пакеты достигнут точки назначения в том же порядке, в котором они были отправлены. Он называется «ненадежным протоколом» (TCP является «надежным протоколом»), что звучит плохо, но так как он намного быстрее, он может быть полезнее. Существуют приложения, такие как аудио сигнал, в которых не критично, если несколько пакетов потеряются здесь или там, а скорость жизненно необходима. Или например сервер времени, для которого реально не имеет значения, если одно из сообщений будет потеряно. Также, некоторые приложения могут быть способны отправлять UDP сообщения к серверу и затем считать, если нет ответа в разумный период времени, что сообщения были потеряны.


Обычно вы будете выполнять ваше прямое сетевое программирование с помощью TCP, и только иногда вы будете использовать UDP. Есть более общее толкование UDP, включая пример, в первой редакции этой книги (доступра на CR-ROM’е, сопровождающем это книгу или может быть свободно загружено с www.BruceEckel.com).

Использование URL’ов из апплета

Для апплета есть возможность стать причиной отображения любого URL с помощью Web броузера, в котором запущен апплет. Вы можете сделать это с помощью следующей строки:

в которой u является объектом типа URL. Вот простой пример, который перенаправляет вас на другую страницу. Хотя вы просто перенаправляете на HTML страницу, вы можете также перенаправить на вывод, который дает CGI программа.

public class ShowHTML extends JApplet <
JButton send = new JButton ( «Go» ) ;
JLabel l = new JLabel () ;

public void init () <
Container cp = getContentPane () ;
cp.setLayout ( new FlowLayout ()) ;
send.addActionListener ( new Al ()) ;
cp.add ( send ) ;
cp.add ( l ) ;
>

class Al implements ActionListener <
public void actionPerformed ( ActionEvent ae ) <
try <
// Это может быть CGI программа вместо
// HTML страницы.
URL u = new URL ( getDocumentBase () , «FetcherFrame.html» ) ;
// Отображается вывод URL с помощью
// Web броузера, как обычная страница:
getAppletContext () .showDocument ( u ) ;
>
catch ( Exception e ) <
l.setText ( e.toString ()) ;
>
>
>

public static void main ( String [] args ) <
Console.run ( new ShowHTML () , 100 , 50 ) ;
>
> // /:

Красота класса URL состоит в том, что он отлично защищает вас. Вы можете соединится с Web серверами без знания многого из того, что происходит за занавесом.

Чтение файла с сервера

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

public class Fetcher extends JApplet <
JButton fetchIt = new JButton ( «Fetch the Data» ) ;
JTextField f = new JTextField ( «Fetcher.java» , 20 ) ;
JTextArea t = new JTextArea ( 10 , 40 ) ;

public void init () <
Container cp = getContentPane () ;
cp.setLayout ( new FlowLayout ()) ;
fetchIt.addActionListener ( new FetchL ()) ;
cp.add ( new JScrollPane ( t )) ;
cp.add ( f ) ;
cp.add ( fetchIt ) ;
>

public class FetchL implements ActionListener <
public void actionPerformed ( ActionEvent e ) <
try <
URL url = new URL ( getDocumentBase () , f.getText ()) ;
t.setText ( url + «n» ) ;
InputStream is = url.openStream () ;
BufferedReader in = new BufferedReader (
new InputStreamReader ( is )) ;
String line;
while (( line = in.readLine ()) != null )
t.append ( line + «n» ) ;
>
catch ( Exception ex ) <
t.append ( ex.toString ()) ;
>
>
>

public static void main ( String [] args ) <
Console.run ( new Fetcher () , 500 , 300 ) ;
>
> // /:

Создание объекта URL похоже на предыдущий пример — getDocumentBase( ) является начальной точкой, как и прежде, но в то же время, имя файла читается из JTextField. Как только объект URL создан, его строковая версия помещается в JTextArea, так что вы можем видеть, как он выглядит. Затем из URL’а получается InputStream, который в данном случае может просто производить поток символов из файла. После конвертации в Reader и буферизации, каждая строка читается и добавляется в JTextArea. Обратите внимание, что JTextArea помещается внутрь JScrollPane, так что скроллирование обрабатывается автоматически.

Мультиплексирование, Основанное на Переключении в JDK 1.4

Когда вы читаете из сокета или пишете в него, вам нужно сделать передачу данных рациональной. Давайте рассмотрим сначала операцию записи. Когда вы пишите данные на уровне приложения (TCP или UDP сокет), вы пишите данные в рабочий буфер системы. Эти данные, в конечном счете, формируют (TCP или UDP) пакеты, которые необходимо передать на машину назначения по сети. Когда вы пишите в сокет и, если в буфере нет достаточно доступного места, запись может блокироваться. Если вы читаете из сокета и нет достаточного количества информации для чтения из буфера операционной системы, куда попадают данные после получения из сети, чтение будет блокировано. Если есть нить (поток) для операции чтения или записи, эта нить не может делать ничего и может стать причиной снижения произовдительности вашей программы. До появления JDK 1.4 не было способа вывести такую нить из заблокированного состояния. С помощью каналов вы можете выполнить асинхронную операцию закрытия на канале и нить, блокированная на этом канале примет AsynchronousCloseException.

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

Что, если вы просто читаете и пишите в дескриптор когда бы вы не захотели? Select может обрабатывать множество дескрипторов, что позволит вам мониторить множество сокетов. Рассмотрим пример чат-сервера, когда сервер имеет соединения с различными клиентами. Тип данных, достигающих сервера, перемежается. Сервер предназначен для чтения данных из сокета и отображения их в GUI, то есть для показа каждому клиенту — чтобы достич этого, вы читаете данные от каджого клиента и пишите эти данные всем остальным клиентам. Например 5 клиентов: 1, 2, 3, 4 и 5. Если сервер запрограммирован на выполнение чтения от 1 и записи в 2, 3, 4 и 5, затем происходит чтения от 2 и запись в 1, 3, 4, 5 и так далее, то может так случиться, что пока нить сервера заблокирована на чтении одного из клиентских сокетов, могут появиться данные на других сокетах. Одно из решений состоит в том, чтобы создавать различные нити для кадого клиента (до JDK1.4). Но это не масштабируемое решение. Вместо этого вы можете иметь селектор, основанный на механизме, следящем за всеми клиентскими сокетами. Он знает какой сокет имеет данные для чтения без блокирования. Но если единственная нить выполняет эту работу (выбор и запись каждому клиенту) он не будет хорошо откликаться. Таким образом в таких ситуациях одна нить мониторит сокеты на чтение, выбирает сокет, из которого можно осуществить чтение, и делегирует остальную ответственность (запись другим клиентам) другой нити (нитям) или пулу нитей.

Этот шаблон называется шаблоном реактора, когда события отсоединяются от действия, ассоциированного с событиями (Pattern Oriented Software Architecture — Doug Schmidt).

В JDK 1.4 вы создаете канал, регестрируете объект Селектора в канале, который (объект) будет следить за событиями в канале. Многие каналы регестрируют один и тот же объект Селектора. Единственная нить, которая вызывает Selector.select(), наблюдает множество каналов. Каждый из классов ServerSocket, Socket и DatagramSocket имеют метод getChannel( ), но он возвращает null за исключением того случая, когда канал создается с помощью вызова метода open( ) (DatagramChannel.open( ), SocketChannel.open( ), ServerSocketChannel.open( )). Вам необходимо ассоциировать сокет с этим каналом.

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

ByteBuffer используется для копирования данных из канала и в канал. ByteBuffer является потоком октетов и вы декодируете этот поток, как символы. Со стороны клиента в MultiJabberClient.java это выполняется путем использования классов Writer’а и OutputStreamWriter’а. Эти классы конвертируют символы в поток байтов.

Приведенная ниже программа NonBlockingIO.java объясняет, как вы можете использовать Селектор и Канал для выполнения мультиплексирования. Эта программа требует запущенного Сервера. Она может стать причиной исключения на сервере, но ее назначение не в коммуникации с сервером, а в том, чтобы показать, как работает select( ).

//: TIEJ:X1:NonBlockingIO.java
// Сокет и Селектор сконфигурированы для не блокированного
// Соединения с JabberServer.java
//
import java.net.*;
import java.nio.channels.*;
import java.util.*;
import java.io.*;

/**
* Цель: Показать как использовать селектор. Нет чтения/записи, просто
* показывается готовность к совершению операции.
*
* Алгоритм: -> Создаем селектор. -> Создаем канал -> Связываем сокет,
* ассоциированный с каналом, с -> Конфигурируем канал, как
* не блокирующий -> Регестрируем канал в селекторе. -> Вызываем метод select( ),
* чтобы он блокировал выполнение до тех пор, пока канал не будет готов. (как
* это предполагается методом select(long timeout) -> Получаем множество ключей,
* относящихся к готовому каналу для работы, основной интерес состоит в том,
* когда они зарегестрированя с помощью селектора. -> Перебираем ключи. -> Для
* каждого ключа проверяем, что соответствующий канал готов к работе, в которой
* он заинтересован. -> Если он готов, печатаем сообщение о готовности.
*
* Примечание: -> Необходим запущенный MultiJabberServer на локальной машине. Вы
* запускаете его и соединяетесь с локальным MultiJabberServer -> Он может стать
* причиной исключения в MultiJabberServer, но это исключение ожидаемо.
*/
public class NonBlockingIO <
public static void main ( String [] args ) throws IOException <
if ( args.length 2 ) <
System.out.println ( «Usage: java » ) ;
System.exit ( 1 ) ;
>
int cPort = Integer.parseInt ( args [ 0 ]) ;
int sPort = Integer.parseInt ( args [ 1 ]) ;
SocketChannel ch = SocketChannel.open () ;
Selector sel = Selector.open () ;
try <
ch.socket () .bind ( new InetSocketAddress ( cPort )) ;
ch.configureBlocking ( false ) ;
// Канал заинтересован в выполнении чтения/записи/соединении
ch.register ( sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE
| SelectionKey.OP_CONNECT ) ;
// Разблокируем, когда готовы к чтению/записи/соединению
sel.select () ;
// Ключи, относящиеся к готовому каналу, канал заинтересован
// в работе, которая может быть выполненаin can be
// без блокирования.
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) <
SelectionKey key = ( SelectionKey ) it.next () ;
it.remove () ;
// Если связанный с ключом канал готов к соединению?
// if((key.readyOps() & SelectionKey.OP_CONNECT) != 0) <
if ( key.isConnectable ()) <
InetAddress ad = InetAddress.getLocalHost () ;
System.out.println ( «Connect will not block» ) ;
// Вы должны проверить возвращаемое значение,
// чтобы убедиться, что он соединен. Этот не блокированный
// вызов может вернуться без соединения, когда
// нет сервера, к которому вы пробуете подключиться
// Поэтому вы вызываете finishConnect(), который завершает
// операцию соединения.
if ( !ch.connect ( new InetSocketAddress ( ad, sPort )))
ch.finishConnect () ;
>
// Если канал, связанный с ключом, готов к чтению?
// if((key.readyOps() & SelectionKey.OP_READ) != 0)
if ( key.isReadable ())
System.out.println ( «Read will not block» ) ;
// Готов ли канал, связанный с ключом, к записи?
// if((key.readyOps() & SelectionKey.OP_WRITE) != 0)
if ( key.isWritable ())
System.out.println ( «Write will not block» ) ;
>
>
finally <
ch.close () ;
sel.close () ;
>
>
> // /:

Как указано выше, вам необходимо создать канал, используя вызов метода open( ). SocketChannel.open( ) создает канал. Так как он наследован от AbstractSelectableChannel (DatagramChannel и SocketChannel), он имеет функциональность для регистрации себя в селекторе. Вызов метода регистрации совершает это. В качестве аргумента он принимает Селектор для регистрации канала, и события, которые интересны для этого канала. Здесь показано, что SocketChannel заинтересован в соединении, чтении и записи — поэтому в вызове метода регистрации указано SelectionKey.OP_CONNECT, SelectionKey.OP_READ и SelectionKey.OP_WRITE наряду с Селектором.

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

Следующий пример работает так же, как и JabberClient1.java, но использует Селектор.

//: TIEJ:X1:JabberClient1.java
// Очень простой клиент, которй просто посылает строки на сервер
// и читает строки, посылаемые сервером.
//
import java.net.*;
import java.util.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

public class JabberClient1 <
public static void main ( String [] args ) throws IOException <
if ( args.length 1 ) <
System.out.println ( «Usage: java JabberClient1 » ) ;
System.exit ( 1 ) ;
>
int clPrt = Integer.parseInt ( args [ 0 ]) ;
SocketChannel sc = SocketChannel.open () ;
Selector sel = Selector.open () ;
try <
sc.configureBlocking ( false ) ;
sc.socket () .bind ( new InetSocketAddress ( clPrt )) ;
sc.register ( sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE
| SelectionKey.OP_CONNECT ) ;
int i = 0 ;
// По причине ассинхронной природы, вы не знаете
// когда чтение и запись закончены, поэтому вам необходимо
// следить за этим, переменная boolean written используется для
// переключения между чтением и записью. Во время записи
// отосланные назад символы должны быть прочитаны.
// Переменная boolean done используется для проверки, когда нужно
// прервать цикл.
boolean written = false, done = false ;
// JabberServer.java, которому этот клиент подсоединяется, пишет с
// помощью
// BufferedWriter.println(). Этот метод выполняет
// перекодировку в соответствии с кодовой страницей по умолчанию
String encoding = System.getProperty ( «file.encoding» ) ;
Charset cs = Charset.forName ( encoding ) ;
ByteBuffer buf = ByteBuffer.allocate ( 16 ) ;
while ( !done ) <
sel.select () ;
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) <
SelectionKey key = ( SelectionKey ) it.next () ;
it.remove () ;
sc = ( SocketChannel ) key.channel () ;
if ( key.isConnectable () && !sc.isConnected ()) <
InetAddress addr = InetAddress.getByName ( null ) ;
boolean success = sc.connect ( new InetSocketAddress (
addr, JabberServer.PORT )) ;
if ( !success )
sc.finishConnect () ;
>
if ( key.isReadable () && written ) <
if ( sc.read (( ByteBuffer ) buf.clear ()) > 0 ) <
written = false ;
String response = cs
.decode (( ByteBuffer ) buf.flip ()) .toString () ;
System.out.print ( response ) ;
if ( response.indexOf ( «END» ) != — 1 )
done = true ;
>
>
if ( key.isWritable () && !written ) <
if ( i 10 )
sc.write ( ByteBuffer.wrap ( new String ( «howdy » + i
+ ‘n’ ) .getBytes ())) ;
else if ( i == 10 )
sc.write ( ByteBuffer.wrap ( new String ( «ENDn» )
.getBytes ())) ;
written = true ;
i++;
>
>
>
>
finally <
sc.close () ;
sel.close () ;
>
>
> // /:

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

//: TIEJ:X1:MultiJabberServer1.java
// Имеет туж е семантику, что и многопоточный
// MultiJabberServer
//
import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.*;

/**
* Сервер принимает соединения не блокирующим способом. Когда соединение
* установлено, создается сокет, который регистрируется с селектором для
* чтения/записи. Чтение/запись выполняется над этим сокетом, когда селектор
* разблокируется. Эта программа работает точно так же, как и MultiJabberServer.
*/
public class MultiJabberServer1 <
public static final int PORT = 8080 ;

public static void main ( String [] args ) throws IOException <
// Канал будет читать данные в ByteBuffer, посылаемые
// методом PrintWriter.println(). Декодирование этого потока
// байт требует кодовой страницы для кодировки по умолчанию.
String encoding = System.getProperty ( «file.encoding» ) ;
// Инициализируем здесь, так как мы не хотим создавать новый
// экземпляр кодировки каждый раз, когда это необходимо
// Charset cs = Charset.forName(
// System.getProperty(«file.encoding»));
Charset cs = Charset.forName ( encoding ) ;
ByteBuffer buffer = ByteBuffer.allocate ( 16 ) ;
SocketChannel ch = null ;
ServerSocketChannel ssc = ServerSocketChannel.open () ;
Selector sel = Selector.open () ;
try <
ssc.configureBlocking ( false ) ;
// Локальныйы адрес, на котором он будет слушать соединения
// Примечание: Socket.getChannel() возвращает null, если с ним не
// ассоциирован канал, как показано ниже.
// т.е выражение (ssc.socket().getChannel() != null) справедливо
ssc.socket () .bind ( new InetSocketAddress ( PORT )) ;
// Канал заинтересован в событиях OP_ACCEPT
SelectionKey key = ssc.register ( sel, SelectionKey.OP_ACCEPT ) ;
System.out.println ( «Server on port: » + PORT ) ;
while ( true ) <
sel.select () ;
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) <
SelectionKey skey = ( SelectionKey ) it.next () ;
it.remove () ;
if ( skey.isAcceptable ()) <
ch = ssc.accept () ;
System.out.println ( «Accepted connection from:»
+ ch.socket ()) ;
ch.configureBlocking ( false ) ;
ch.register ( sel, SelectionKey.OP_READ ) ;
>
else <
// Обратите внимание, что не выполняется проверка, если
// в канал
// можно писать или читать — для упрощения.
ch = ( SocketChannel ) skey.channel () ;
ch.read ( buffer ) ;
CharBuffer cb = cs.decode (( ByteBuffer ) buffer.flip ()) ;
String response = cb.toString () ;
System.out.print ( «Echoing : » + response ) ;
ch.write (( ByteBuffer ) buffer.rewind ()) ;
if ( response.indexOf ( «END» ) != — 1 )
ch.close () ;
buffer.clear () ;
>
>
>
>
finally <
if ( ch != null )
ch.close () ;
ssc.close () ;
sel.close () ;
>
>
> // /:

Здесь приведена простейшая реализация Пула Нитей. В этой реализации нет полинга (занят-ожидает) нитей. Она полностью основана на методах wait( ) и notify( ).

//: TIEJ:X1:Worker.java
// Instances of Worker are pooled in threadpool
//
//
import java.io.*;
import java.util.logging.*;

public class Worker extends Thread <
public static final Logger logger = Logger.getLogger ( «Worker» ) ;
private String workerId;
private Runnable task;
// Необходима ссылка на пул нитей в котором существует нить, чтобы
// нить могла добавить себя в пул нитей по завершению работы.
private ThreadPool threadpool;
static <
try <
logger.setUseParentHandlers ( false ) ;
FileHandler ferr = new FileHandler ( «WorkerErr.log» ) ;
ferr.setFormatter ( new SimpleFormatter ()) ;
logger.addHandler ( ferr ) ;
>
catch ( IOException e ) <
System.out.println ( «Logger not initialized..» ) ;
>
>

public Worker ( String id, ThreadPool pool ) <
worker > threadpool = pool;
start () ;
>

// ThreadPool, когда ставит в расписание задачу, использует этот метод
// для делегирования задачи Worker-нити. Кроме того для установки
// задачи (типа Runnable) он также переключает ожидающий метод
// run() на начало выполнения задачи.
public void setTask ( Runnable t ) <
task = t;
synchronized ( this ) <
notify () ;
>
>

public void run () <
try <
while ( !threadpool.isStopped ()) <
synchronized ( this ) <
if ( task != null ) <
try <
task.run () ; // Запускаем задачу
>
catch ( Exception e ) <
logger.log ( Level.SEVERE,
«Exception in source Runnable task» , e ) ;
>
// Возвращает себя в пул нитей
threadpool.putWorker ( this ) ;
>
wait () ;
>
>
System.out.println ( this + » Stopped» ) ;
>
catch ( InterruptedException e ) <
throw new RuntimeException ( e ) ;
>
>

public String toString () <
return «Worker : » + workerId;
>
> // /:

Основной алгоритм:
while true:

  1. Проверить очередь задач.
  2. Если она пуста, подождать, пока в очередь будет добавлена задача.
    (вызов метода addTask( ) добавляет задачу и уведомляет очередь для разблокирования)
  3. Пробуем получить рабочую (Worker) нить из пула нитей.
  4. Если нет ни одной доступной нити, ожидаем в пуле нитей.
    (Когда нить освободится, она уведомит пул нитей для разблокировки)
  5. На этой стадии есть задачи в очереди и есть свободная рабочая нить.
  6. Делегируем задачу из очереди рабочей нити.

//: TIEJ:X1:ThreadPool.java
// Пул нитей, которые выполняют задачи.
//
import java.util.*;

public class ThreadPool extends Thread <
private static final int DEFAULT_NUM_WORKERS = 5 ;
private LinkedList workerPool = new LinkedList () ,
taskList = new LinkedList () ;
private boolean stopped = false ;

public ThreadPool () <
this ( DEFAULT_NUM_WORKERS ) ;
>

public ThreadPool ( int numOfWorkers ) <
for ( int i = 0 ; i )
workerPool.add ( new Worker ( «» + i, this )) ;
start () ;
>

public void run () <
try <
while ( !stopped ) <
if ( taskList.isEmpty ()) <
synchronized ( taskQueue ) <
// Если очередь пустая, подождать, пока будет добавлена
// задача
taskList.wait () ;
>
>
else if ( workerPool.isEmpty ()) <
synchronized ( workerPool ) <
// Если нет рабочих нитей, подождать, пока
// пока не появится
workerPool.wait () ;
>
>
// Запускаем следующую задачу из расписания задач
getWorker () .setTask (( Runnable ) taskList.removeLast ()) ;
>
>
catch ( InterruptedException e ) <
throw new RuntimeException ( e ) ;
>
>

public void addTask ( Runnable task ) <
taskList.addFirst ( task ) ;
synchronized ( taskList ) <
taskList.notify () ; // Если добавлена новая задача, уведомляем
>
>

public void putWorker ( Worker worker ) <
workerPool.addFirst ( worker ) ;
// Здесь может быть случай, когда вы будете иметь пул из 5 нитей,
// а будет требоваться больше. Это происходит тогда, когда требуется
// рабочая нить,
// но ее нет (свободной), тогда просто блокируем пул нитей.
// Это событие, при котором появляется свободная рабочая нить в пуле
// нитей
// Поэтому эта нить посылает уведомление и разблокирует
// нить ThreadPool, ожидающую пул нитей
synchronized ( workerPool ) <
workerPool.notify () ;
>
>

private Worker getWorker () <
return ( Worer ) workerPool.removeLast () ;
>

public boolean isStopped () <
return stopped;
>

public void stopThreads () <
stopped = true ;
Iterator it = workerPool.iterator () ;
while ( it.hasNext ()) <
Worker w = ( Worker ) it.next () ;
synchronized ( w ) <
w.notify () ;
>
>
> // Junit test

public void testThreadPool () <
ThreadPool tp = new ThreadPool () ;
for ( int i = 0 ; i 10 ; i++ ) <
tp.addTask ( new Runnable () <
public void run () <
System.out.println ( «A» ) ;
>
>) ;
>
tp.stopThreads () ;
>
> // /:

Следующий пример MultiJabberServer2.java использует пул нитей. Это шаблон Реактора. Как установлено выше, события отделяются от ассоциированных с ними действий. Пул нитей ассинхронно разделяет действия, ассоциированные с событиями. В системах масштаба предприятия такое разделение обычно достигается путем использования Системы Cообщений Java — Java Messaging System (JMS).

//: TIEJ:X1:MultiJabberServer2.java
// Семантика аналогична MultiJabberServer1, с использованием пула нитей.
//
import java.io.*;

class ServeOneJabber implements Runnable <
private SocketChannel channel;
private Selector sel;

public ServeOneJabber ( SocketChannel ch ) throws IOException <
channel = ch;
sel = Selector.open () ;
>

public void run () <
ByteBuffer buffer = ByteBuffer.allocate ( 16 ) ;
boolean read = false, done = false ;
String response = null ;
try <
channel.register ( sel, SelectionKey.OP_READ | SelectionKey.OP_WRITE ) ;
while ( !done ) <
sel.select () ;
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) <
SelectionKey key = ( SelectionKey ) it.next () ;
it.remove () ;
if ( key.isReadable () && !read ) <
if ( channel.read ( buffer ) > 0 )
read = true ;
CharBuffer cb = MultiJabberServer2.CS
.decode (( ByteBuffer ) buffer.flip ()) ;
response = cb.toString () ;
>
if ( key.isWritable () && read ) <
System.out.print ( «Echoing : » + response ) ;
channel.write (( ByteBuffer ) buffer.rewind ()) ;
if ( response.indexOf ( «END» ) != — 1 )
done = true ;
buffer.clear () ;
read = false ;
>
>
>
>
catch ( IOException e ) <
// будет поймано Worker.java и залогировано.
// Необходимо выбросить исключение времени выполнения, так как мы не
// можем
// оставить IOException
throw new RuntimeException ( e ) ;
>
finally <
try <
channel.close () ;
>
catch ( IOException e ) <
System.out.println ( «Channel not closed.» ) ;
// Выбрасываем это, чтобы рабочая нить могла залогировать.
throw new RuntimeException ( e ) ;
>
>
>
>

public class MultiJabberServer2 <
public static final int PORT = 8080 ;
private static String encoding = System.getProperty ( «file.encoding» ) ;
public static final Charset CS = Charset.forName ( encoding ) ;
// Создаем пул нитей с 20 рабочими нитями.
private static ThreadPool pool = new ThreadPool ( 20 ) ;

public static void main ( String [] args ) throws IOException <
ServerSocketChannel ssc = ServerSocketChannel.open () ;
Selector sel = Selector.open () ;
try <
ssc.configureBlocking ( false ) ;
ssc.socket () .bind ( new InetSocketAddress ( PORT )) ;
SelectionKey key = ssc.register ( sel, SelectionKey.OP_ACCEPT ) ;
System.out.println ( «Server on port: » + PORT ) ;
while ( true ) <
sel.select () ;
Iterator it = sel.selectedKeys () .iterator () ;
while ( it.hasNext ()) <
SelectionKey skey = ( SelectionKey ) it.next () ;
it.remove () ;
if ( skey.isAcceptable ()) <
SocketChannel channel = ssc.accept () ;
System.out.println ( «Accepted connection from:»
+ channel.socket ()) ;
channel.configureBlocking ( false ) ;
// Отделяем события и ассоциированное действие
pool.addTask ( new ServeOneJabber ( channel )) ;
>
>
>
>
finally <
ssc.close () ;
sel.close () ;
>
>
> // /:

Это минимальное обновления для JabberServer.java. Изначально, когда клиент посылает ‘END’, JabberServer не отправляет его назад. Эта версия JabberServer отсылает строку ‘END’ назад. Эти изменения были сделаны, чтобы упростить JabberClient1.java.

//: TIEJ:X1:JabberServer.java
// Очень простой сервер, который просто
// отсылает назад то, что получил от клиента.
//
import java.io.*;

public class JabberServer <
// Выбираем порт за пределами диапазона 1-1024:
public static final int PORT = 8080 ;

public static void main ( String [] args ) throws IOException <
ServerSocket s = new ServerSocket ( PORT ) ;
System.out.println ( «Started: » + s ) ;
try <
// Блокируем до возникновения соединения:
Socket socket = s.accept () ;
try <
System.out.println ( «Connection accepted: » + socket ) ;
BufferedReader in = new BufferedReader ( new InputStreamReader (
socket.getInputStream ())) ;
// Вывод автоматически выталкивается PrintWriter’ом:
BufferedWriter out = new BufferedWriter ( new OutputStreamWriter (
socket.getOutputStream ())) ;
while ( true ) <
String str = in.readLine () ;
System.out.println ( «Echoing: » + str ) ;
out.write ( str, 0 , str.length ()) ;
out.newLine () ;
out.flush () ;
if ( str.equals ( «END» ))
break ;
>
// Всегда закрываем два сокета.
>
finally <
System.out.println ( «closing. » ) ;
socket.close () ;
>
>
finally <
s.close () ;
>
>
> // /:

Еще о работе с сетью

На самом деле есть очень много тем, касающихся сетевой работы, которые могут быть освещены в этой вводной статье. Сетевая работа Java также предоставляет четкую и всестороннюю поддержку для URL, включая обработчики протоколов для различных типов содержимого, которое может быть доступно на Интернет сайте. Вы можете найти полное и подробное описание других особенностей сетевого взаимодействия Java в книге Elliotte Rusty Harold «Java Network Programming» (O’Reilly, 1997).

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

Продолжаем изучать стандартную библиотеку языка программирования Java. И сегодня речь пойдет о многопоточности:

  • что такое многопоточность;
  • как ее реализовать;
  • как создать о остановить потоки выполнения.

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

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

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


Многопоточность в Java. Основы

— Преимущества многопоточности
— Создание потоков
— Прерывание потоков
— Проблема доступа к общим ресурсам
— Синхронизация. Пример проектирования многопоточного приложения
— Вариант 1. Без многопоточности
— Вариант 2. Один файл — один поток
— Вариант 3. Использование синхронизированного списка
— Вариант 4. Ограничение количества потоков
— Вариант 5. java.util.concurrent
— Выводы
Существуют понятия процесс и поток. Процессу выделяется память, какие-то ресурсы, а также он содержит по крайней мере один поток. У потока же есть набор инструкций для выполнения кода.
Поскольку у одного процесса может быть несколько потоков, а ресурсы у процесса одни, то все эти потоки будут работать с этими одними ресурсами, причём почти одновременно. Здесь и кроется вся сложность создания многопоточных приложений.

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

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

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

Что ж, достаточно теории, перейдём к практике.

Создание потоков
В Java для работы с потоками служит класс Thread . Есть два способа создания нового потока, но их можно записать куда большим числом способов.

Первый вариант, это расширение класса Thread.

Как видно, во втором случае приходится чуть больше писать, но на самом деле преимущество всё-таки у этого способа, потому что мы можем и расширить класс, и реализовать интерфейс Runnable (например, class CustomRunnable extends JPanel implements Runnable ). Первый же способ удобен в том случае, если надо переопределить или добавить новые методы в класс Thread.

Если новый класс для потока создавать накладно, то можно сделать всё в анонимном классе.

Эти два способа хорошо подходят для случаев, когда кода в методе run очень мало.

В Java 8 появились лямбда-выражения, а вместе с ними и ещё более короткие способы создания потока:

Можно ещё побаловаться с Reflection API, но это уж как-нибудь сами.

Прерывание потоков
Да-да, именно прерывание, а не остановка или пауза. Потоки нельзя остановить стандартными средствами, потому что с этим связано множество проблем. Поэтому методы stop, suspend, pause и destroy в классе Thread помечены как deprecated . Так что остановку приходится делать самому.

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

И, к слову, если поток работает в фоне и вы закрываете программу, то процесс этой программы всё равно останется висеть в фоне, пока не завершатся все потоки. Чтобы по окончанию программы гарантированно получить завершение потоков, нужно сделать из потока демона . Звучит забавно, но так и есть:

Вы уже заметили, что во всех блоках catch я пишу код Thread.currentThread().interrupt(); ? Это хорошая практика. Если во время паузы произошло исключение InterruptedException, то вы помечаете поток как interrupted и тем самым завершаете его выполнение (если использовали Thread.interrupted() ). Если же оставлять блок catch пустым, то ничего не пометится. В некоторых случаях это может быть оправдано, но в остальном, рекомендую писать так.

С учётом исключения InterruptedException, можно переписать первый пример:

При исключении мы покинем цикл while и поток завершится. Оба метода runnable.stop() и thread.interrupt() теперь успешно завершают поток. Обратите внимание на ключевое слово volatile для поля isRunning, я объясню его значение чуть позже.

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

Первая проблема — доступ к общим ресурсам из нескольких потоков. Допустим, есть некоторый счётчик:

Почему так?
Тут вступает проблема видимости объекта.
Дело в том, что, изменив поле одним потоком, второй не сразу может увидеть это изменение или увидит его в другом порядке. Это связано с другим понятием атомарность .

Атомарной называется такая операция, которая может быть выполнена за один неделимый шаг.
Например, атомарной операцией будет присвоение числу int некоторой константы. А неатомарной — сумма двух чисел.
Чтобы пояснить, я окунусь глубже в JVM.
Некоторые операции в Java состоят из нескольких инструкций байт-кода:

Подвох вот в чём. В первом случае изменение поля происходит за одну инструкцию putstatic. Во втором, между getstatic и putstatic аж две инструкции. Ничто не мешает другому потоку, пока выполняется сложение, перезаписать значение поля counter.

Детальнее обо всём этом можно прочесть в статье: Модель памяти в примерах и не только / Хабрахабр

На 32-битных системах, присвоение значения типу long или double тоже будет неатомарной операцией. Но объявив поле с ключевым словом volatile , JVM будет гарантировать атомарность и видимость всем простым операциям, а именно чтению и записи поля. Вот только в нашем случае со счётчиком это не поможет — операция инкремента неатомарна.

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

Для объявления блоков синхронизации в Java служит ключевое слово synchronized :

Теперь, чтобы решить нашу проблему со счётчиком, достаточно добавить неатомарную операцию в блок синхронизации.

На самом деле в этом коде тоже есть проблема, попробуйте найти её после прочтения раздела. Или читайте комментарии к статье.

Как это будет работать:
— Мы создали два потока и запустили их.
— Допустим, первый поток запустился быстрее и вошёл в цикл while. Второй пока запускается.
— Первый поток видит блок synchronized. Выполняется проверка — нет ли сейчас в этом блоке других потоков? Нет, поэтому первый поток заходит в блок. Второй пока что вошёл в цикл while.
— Первый поток сейчас в цикле for увеличивает счётчик. Второй поток доходит до блока synchronized. Снова выполняется проверка и поскольку поток внутри есть, разрешение войти внутрь не получено, а значит второй поток ждёт.
— Первый поток всё ещё в цикле for. Второй поток всё так же ждёт.
— Наконец, первый поток выходит из цикла for и покидает область синхронизации. Второй поток получает разрешение войти внутрь.

Таким образом, получается синхронизированная работа потоков.

Блоки синхронизации следует расставлять с умом. Если бы мы сделали вот так:

мы бы тоже получили верный результат 5000, вот только работал бы у нас только один поток:
— Создаём два потока и запускаем их.
— Допустим, первый поток запустился быстрее и вошёл в блок синхронизации. Второй пока запускается.
— Первый поток теперь в цикле while. Второй поток встретил блок synchonized и не получил разрешение войти.
— Первый поток работает. Второй ждёт.
— Спустя некоторое количество времени, первый поток увеличил счётчик до 5000 и вышел из циклов и блока синхронизации. Второму потоку разрешается войти внутрь.
— Первый поток завершил работу. Второй поток проверил, что условие Counter.get() уже не выполняется и не вошёл в цикл while. Покинул блок синхронизации и завершился.

Другой вариант решения проблемы со счётчиком — сделать его методы get и increment синхронизированными. Тогда блок синхронизации в методе run не понадобится.

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

В байткоде synchronized метода инструкций monitorenter и monitorexit не было, но это не значит, что нет входа в монитор. Флаг SYNCHRONIZED у метода говорит JVM о том, что все эти инструкции нужно выполнить. То есть, они не появляются в коде, но сокрыты в JVM — она всё равно их выполнит.

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

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

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

Есть класс, отвечающий за панель рисования, туда мы будем добавлять объекты по мере чтения:

▌ Вариант 1. Без многопоточности

Казалось бы, нет потоков — нет проблем — будет работать медленно, но зато стабильно. Не тут-то было. При работе иногда выскакивает ошибка:

Ошибка ConcurrentModificationException появляется в том случае, когда предпринимается попытка доступа к списку из разных потоков.
Но погодите-ка, мы ведь не создавали потоков? Мы — нет, а вот для отрисовки интерфейса служит другой поток.

Теперь ошибок нет, но обработка 20 файлов занимает порядка пяти минут. Причём, первые файлы читаются быстро, а потом работа замедляется. Попробуйте понять, почему так происходит.

▌ Вариант 2. Один файл — один поток

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

Тем не менее, время работы теперь: 40 секунд. Это долго. Та же проблема, что замедляла работу в первом варианте, замедляет всё и сейчас. Должен быть способ избавиться от synchronized блоков и такой способ есть.

▌ Вариант 3. Использование синхронизированного списка

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

Время работы теперь чуть менее 4 секунд!

▌ Вариант 4. Ограничение количества потоков

Мы можем ограничить количество потоков, создав пул:

Теперь можно создать хоть сотню потоков, выполняться в один момент времени будут не более maxThreads потоков.

Для добавления в пул существует метод execute(Runnable r) . Пул потоков нужно завершать методом shutdown или shutdownNow . Метод awaitTermination(long timeout, TimeUnit unit) ждёт завершения потоков либо указанное в параметрах время, если к этому моменту есть работающие задачи.
Подробнее об Executor и ExecutorService я постараюсь рассказать в следующей статье.

Ограничим пул пятью потоками

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

Лучшей практикой, является ограничение пула потоков по количеству процессоров в системе:

▌ Вариант 5. java.util.concurrent

И снова я забегу вперёд и возьму что-нибудь из пакета java.util.concurrent. В этом пакете есть класс CopyOnWriteArrayList . Он настолько суров, что блоки синхронизации ему не нужны и можно беспрепятственно перебирать элементы в foreach.

Особых изменений в скорости работы при нескольких потоках мы не получим, но если запустить всё в одном потоке, как в первом случае, то вместо пяти минут мы получим 55 секунд!

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

Наглядная работа всех вариантов представлена в этой анимации:

Запуск нескольких потоков одновременно

Здравствуйте. Подскажите пожалуйста как запустить несколько потоков в такой ситуации.
Есть некий путь к папке, необходимо по этому пути посчитать количество файлов, но для каждой ссылки- отдельный поток.
Я реализовал поток с параметром, где каждому новому потоку передаю уникальную строку пути.Но, у меня потоки выполняются не одновременно, а по очереди.
К примеру, 4 путь и 4 поток, перед этим были 1,2,3, которые запускались после завершение предыдущего.

Код запуска потоков:

10.09.2014, 21:22

Запуск нескольких потоков одновременно
Здравствуйте. У меня есть стэк объектов, с которыми нужно произвести какие-то действия. Причем.

Запуск нескольких потоков Java
Всем доброго времени суток! Наверное все знакомы с задачей Producer — Consumer. Если нет то вот.

Выполнение нескольких потоков
Объясните каким образом выполняется данный код: class DaughterThread implements Runnable< String.

Запуск потоков из цикла
Добрый день. Пытаюсь реализовать много поточное движение неких объектов. В классе объекта: .

Как дождаться завершения нескольких потоков
Добрый день. Столкнулся с такой проблемой: в методе main() создается n одинаковых потоков, каждый.

Java для белок

23 января 2012 г.

Подготовка к собеседованию на java-программиста-2 (уровень M >

Продолжаем популярную тему собеседований. На этот раз мне попалась компания, в которой большое внмание уделяется многопоточности. Под катом вы найдете 15 вопросов, которые были мне заданы на должность Staff Java developer.

В чем отличие между static synchronized и просто synchronized?

Использование слова synchronize на static-методе помечает весь класс как synchronize, т.е. такая запись:
равнозначна такой:
А такая:
такой:
Что значит synchronized(this)?

Семантически этот код:
эквивалентен этому:
Однако synchronized(this) использовать не рекомендуется. Почему? Об этом можно почитать тут .

Какие модификаторы доступа устанавливаются по умолчанию при описании интерфейсов и какие вообще бывают модификаторы доступа у интерфейсов?

Все члены интерфейса по умолчанию являются public.

Методы, объявленные в составе интерфейса, неявно получают признак abstract. В соответствии с принятым соглашением подификатор abstract в объявлении не указывается.

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

Другие модификаторы доступа в интерфейсах не доступны.

Что вернет hashCode(), если его не перегружать?

Адрес объекта в памяти.

Как реализовать множественное наследование в Java?

Множественное наследование реализуется через внутренние классы. Предположим, у вас есть классы Parent1 и Parent2, от которых вы хотите унаследоваться. Тогда код мог бы выглядеть так:
Теперь у экземпляров класса MyClass будут методы и Parent1 и Parent2.

Что означают ключевые слова volatile и transient.

Определение переменной с ключевым словом volatile(«изменчивый») означает, что значение переменной будет изменяться несколькими потоками.

Переменная помеченая словом transient не будет сериализоваться и соответственно при десериализации её значение будет установлено значением по умолчанию.

Условия сериализации объектов?

Свойства класса должны быть сериализуемы и класс должен реализовывать маркирующий интерфейс Serializable. Не лишним будет вспомнить про serialVersionUID и его значение по умолчанию.

Можно ли передать объект в synchronized?

Да. Например очень популярна вот такая несложная конструкция:
Работа со ссылками (weak и strong reference).

Об этом можно почитать тут.

Как сделать ArrayList синхронизированым в одну строку?
Контракт между equals и hashCode.

Если equals возвращает true, то hashCode должен вернуть одно и то же значение. Обратное не верно.

Каким условиям должен отвечать объект, чтобы с ним можно было работать в TreeSet?

Чтобы работать с TreeSet нужно чтобы классы коллекции реализовывали интерфейс Comparable.

Если объект этот интерфейс не реализует, но нужно обеспечить его работу в TreeSet, то компаратор пишется отдельно и передается конструктору TreeSet как аргумент.

Будет ли вызван код в блоке finally() если в try вызывать System.exit(0)?

Декартово произведение двух таблиц в sql

При декартовом произвеедении каждая строка из первой таблицы соединяется с каждой строкой второй таблицы. Делается это так:
Расскажите про декларативные транзакции.

Илон Маск рекомендует:  Asp разработка производительных приложений isapi
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL