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

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

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

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

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

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

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

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

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

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

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

threads_1

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

threads_2

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

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

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

Оба способа практически равнозначны, однако давайте рассмотрим поподробнее.

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

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

Соответственно, такой же класс можно создать, реализовав интерфейс Runnable:

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

В чем же преимущество того или иного способа? Ну, во-первых, не стоит забывать, что класс может реализовывать несколько интерфейсов и это не накладывает на нас некоторые ограничения, связанные с одиночным наследованием, как в случае, если бы мы наследовались от класса Thread. В общем, конкретных серьезных преимуществ ни у одного из этих способов нет. Но наследуйтесь от класса Thread в тех случаях,  когда вам действительно очень необходим новый вид потока, расширяющий возможности стандартного класса 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 не будет опубликован.