Последовательность операций, выполняемых программой по одной в каждый момент времени, называют потоком (или нитью - thread). До сих пор во всех рассматриваемых примерах подразумевалось, что в один момент времени исполняется лишь одно выражение, или действие. Однако, начиная с самых первых версий, виртуальные машины Java поддерживают многопоточность, т.е. поддержку нескольких потоков исполнения одновременно.
Проблемы однопоточного подхода
• Монопольный захват задачей процессорного времени.
• Смешение логически несвязанных фрагментов кода.
• Попытка их разделения приводит к возникновению в программе новых систем.
Реализацию многопоточной архитектуры проще всего представить себе для системы, в которой есть несколько центральных вычислительных процессоров. В этом случае для каждого из них можно выделить задачу, которую он будет выполнять. В результате несколько задач будут обслуживаться одновременно.
Однако возникает вопрос – каким же тогда образом обеспечивается многопоточность в системах с одним центральным процессором, который в принципе выполняет лишь одно вычисление в один момент времени? В таких системах применяется процедура квантования времени (time-slicing). Время разделяется на небольшие интервалы (кванты времени). Во время одного кванта обрабатывается один поток команд. Перед началом каждого интервала принимается решение, какой именно поток выполнения будет обрабатываться на протяжении этого кванта времени. За счет частого переключения между задачами эмулируется многопоточная архитектура, т.е. создается иллюзия одновременности выполнения.
На самом деле, как правило, и для многопроцессорных систем применяется процедура квантования времени. Дело в том, что даже в мощных серверах приложений процессоров не так много (редко бывает больше десяти), а потоков исполнения запускается, как правило, гораздо больше. Т.о. в многопроцессорной системе поток не занимает монопольно один процессор.
Особенности многопоточности
• Потоки выполняются условно независимо.
• Потоки могут взаимодействовать друг с другом.
• Простота выделения подзадач.
• Более гибкое управление выполнением задач. Предположим, пользователь системы, не поддерживающей многопоточность, решил скачать большой файл из сети или произвести сложное вычисление, что занимает, скажем, 2 часа. Запустив задачу на выполнение, он может внезапно обнаружить, что ему нужен не этот, а какой-нибудь другой файл (или вычисление с другими начальными параметрами). Однако если приложение занимается только работой с сетью (вычислениями) и не реагирует на действия пользователя (не обрабатываются данные с устройств ввода, таких как клавиатура или мышь), то он не сможет быстро исправить свою ошибку. Получается, что процессор выполняет большее количество вычислений, но при этом приносит гораздо меньше пользы своему пользователю.
• Более медленное выполнение? Действительно, для переключения между задачами на каждом интервале требуется дополнительное время, а ведь переключения происходят довольно часто. Если бы процессор, не отвлекаясь, выполнял задачи последовательно, одну за другой, он закончил бы их заметно быстрее. Стало быть, преимущества заключаются не в этом. Первый тип приложений, который выигрывает от поддержки многопоточности, предназначен для задач, где действительно требуется выполнять несколько действий одновременно. Например - активные игры, или подобные приложения. Необходимо одновременно опрашивать клавиатуру и другие устройства ввода, чтобы реагировать на действия пользователя. В то же время одновременно необходимо рассчитывать и перерисовывать изменяющееся состояние игрового поля. Понятно, что в случае отсутствия поддержки многопоточности для реализации подобных приложений потребовалось бы реализовывать квантования времени вручную. Условно говоря - одну секунду проверять состояние клавиатуры, а следующую - пересчитывать и перерисовывать игровое поле. Если сравнить две реализации time-slicing, одну - на низком уровне, выполненную средствами, как правило, операционной системы, другую - выполняемую вручную, на языке высокого уровня, мало подходящего для таких задач, то становится понятным преимущество многопоточности. Она обеспечивает наиболее эффективную реализацию процедуры квантования времени, существенно облегчая и укорачивая процесс разработки приложения. Код переключения между задачами на Java выглядел бы гораздо более громоздко, чем независимое описание действий для каждого потока.
• Выигрыш в скорости выполнения при разделении задач по используемым ресурсам. Это преимущество проистекает из того, что компьютер состоит не только из одного или нескольких процессоров. Вычислительное устройство - лишь одно из ресурсов, необходимых для выполнения задач. Всегда есть оперативная память, дисковая подсистема, сетевые подключения, периферия (принтер и т.п.) и другие. Предположим, пользователю требуется распечатать большой документ и скачать большой файл из сети. Очевидно, что обе задачи требуют совсем небольшого участия процессора, а основные необходимые ресурсы, которые будут использоваться, у них разные - сетевое подключение и принтер. Значит, если выполнять задачи одновременно, то замедление от организации квантования времени будет незначительным, процессор легко справится с обслуживанием обеих задач. В то же время, если каждая задача поотдельности занимала, скажем, 2 часа, то вполне вероятно, что и при одновременном исполнении потребуется не более тех же 2 часов, а сделано при этом будет гораздо больше.
• Недетерминизм при выполнении.
Использование класса Thread
Поток выполнения в Java представляется экземпляром класса Thread. Для того, чтобы написать свой поток исполнения необходимо наследоваться от этого класса и переопределить метод run(). Стандартная реализация метода run не предполагает выполнения каких бы то ни было действий.
Объявление:
public class < Имя класса > extends Thread {
public void run () {
// Действия, выполняемые потоком
}
}
Метод run() содержит действия, которые должны исполняться в новом потоке исполнения. Чтобы запустить его, необходимо создать экземпляр класса-наследника, и вызвать унаследованный метод start(), который сообщает виртуальной машине, что необходимо запустить новый поток исполнения и начать в нем исполнять метод run().
Запуск:
<Имя класса> t = new <Имя класса>();
t.start();
Вызов start для каждого потока может быть осуществлен только один раз – повторное обращение приводит к выбрасыванию исключения типа IllegalThreadsStateException .
Потоку разрешено присвоить имя – либо с помощью аргумента String, переданного конструктору, либо посредством вызова метода setName. Текущее значение имени потока можно получить с помощью метода getName. Имена потоков служат только для удобства программиста (исполняющей системой они не используются), но поток должен обладать именем, и если оно не задано, исполняющая система генерирует имена в соответствии с неким простым правилом, например, thread_1, thread_2 и т.д.
Ссылку на объект Thread текущего выполняемого потока можно получить с помощью статического метода Thread.currentThread. Во время работы программы текущий поток существует всегда, даже если вы не создавали потоки явно – метод main активизируется с помощью потока, создаваемого исполняющей системой.
Дата: 2019-02-19, просмотров: 255.