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

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

Java, в отличие от многих других языков программирования, изначально разрабатывался с поддержкой многопоточности. Многопоточность в Java существует на уровне самого языка. Хотя и на  В цикле статей, посвященных данной теме, мы разберем основу создания приложений с поддержкой параллельных вычислений, модель памяти Java (JMM) и пакет java.util.concurrency, являющийся дополнительным инструментом для использования в многопоточных приложениях.

Процессы и потоки.

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

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

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

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

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

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

Главный поток.

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

В результате вы получите такой вывод:

threads_1

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

threads_2

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

Создание потоков.

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

  1. Наследоваться от специального класса Thread, описывающего физический поток.
  2. Реализовать специальный интерфейс Runnable.

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

Воспользуемся первым способом:

В созданном подклассе следует переопределить метод run(), в котором следует разместить тот код, который должен выполняться параллельно.

Рассмотри теперь второй способ, реализовав интерфейс Runnable:

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

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

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

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

Пример.

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

Итак, для создания потока мы реализуем у нашего класса интерфейс Runnable и метод run(),  в котором выполняем всю работу — выводим числа от 1 до 10 на экран вместе с именем потока, который эту работу выполняет. В конструкторе класса MyThread мы создаем новый объект класса Thread, передав в качестве параметра ссылку на текущий объект типа Runnable и имя нового потока. Затем вызовом метода start() мы запускаем новый поток на выполнение.

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

Так как переключение потоков может различаться на некоторых машинах, то ваш вывод может отличаться:

threads_3

Приоритеты потоков.

Потоками управляет менеджер потоков. По завершении работы одного потока он решает, какой поток запустить следующим. В этом ему может помочь приоритет потока – целочисленное значение от 0 до 10. Чем больше это число, тем выше приоритет и тем больше шансов, что этот поток запустится раньше. По умолчанию это значение равно 5 и соответствует константе NORM_PRIORITY. 0 – минимальный приоритет (MIN_PRIORITY), 10 – максимальный приоритет (MAX_PRIORITY). Узнать текущее значение приоритета вы можете с помощью метода getPriority() класса Thread:

Установить приоритет потока вы можете методом setPriority(int priority).

Вспомогательные методы.

Метод Thread.sleep().

Заставляет поток ожидать в течение определенного времени, указанного в миллисекундах. Например, запись Thread.sleep(1000) означает что поток будет находиться в состоянии ожидания в течение одной секунды.

Метод isAlive().

Выполняет проверку того, запущен ли данный поток или был завершен.

Методы setName() и getName().

Позволяют работать с именами потоков, если это необходимо.

Метод join().

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

Метод Thread.yield() .

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

Daemon-потоки (демоны).

Потоки-демоны — это потоки, который работают в фоновом режиме и выполняют определенную задачу. Чтобы превратить обычный поток в daemon-поток, можно воспользоваться вызовом метода setDaemon().

Данная статья является первой в цикле статей, посвященных многопоточному программированию на языке Java. В следующих статьях мы затронем темы синхронизации, модели памяти Java и дополнительные инструменты для создания многопоточных приложений из пакета java.util.concurrency. До встречи в следующих статьях!

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *