Thread’ом Java не испортишь: Часть I — потоки
Многопоточность в Java была заложена с самых первых дней. Поэтому давайте кратко ознакомимся с тем, про что это — многопоточность. Возьмём за точку отсчёта официальный урок от Oracle: «Lesson: The «Hello World!» Application». Код нашего Hello World приложения немного изменим на следующий:
args — это массив входных параметров, передаваемых при запуске программы. Сохраним данный код в файл с именем, которое совпадает с именем класса и с расширением .java . Скомпилируем при помощи утилиты javac: javac HelloWorldApp.java После этого вызовем наш код с каким-нибудь параметром, например, Roger: java HelloWorldApp Roger У нашего кода сейчас есть серьёзный изъян. Если не передать никакой аргумент (т.е. выполнить просто java HelloWorldApp), мы получим ошибку:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0 at HelloWorldApp.main(HelloWorldApp.java:3)
Возникло исключение (т.е. ошибка) в thread (в потоке) с именем main . Получается, в Java есть какие-то потоки? Отсюда начинается наш путь.
Java и потоки
Чтобы разобраться, что такое поток, надо понять, как происходит запуск Java приложения. Давайте изменим наш код следующим образом:
Теперь давайте скомпилируем это снова при помощи javac. Далее для удобства запустим наш Java код в отдельном окне. В Windows это можно сделать так: start java HelloWorldApp . Теперь при помощи утилиты jps посмотрим, какую информацию нам сообщит Java: Первое число — это PID или Process ID, идентификатор процесса. Что такое процесс?
Процесс — это совокупность кода и данных, разделяющих общее виртуальное адресное пространство.
При помощи процессов выполнение разных программ изолировано друг от друга: каждое приложение использует свою область памяти, не мешая другим программам. Более подробно советую ознакомиться в статье: «https://habr.com/post/164487/». Процесс не может существовать без потоков, поэтому если существует процесс, в нём существует хотя бы один поток. Как же это происходит в Java? Когда мы запускаем Java программу, ее выполнение начинается с метода main . Мы как бы входим в программу, поэтому этот особый метод main называется точкой входа, или «entry point». Метод main всегда должен быть public static void , чтобы виртуальная машина Java (JVM) смогла начать выполнение нашей программы. Подробнее см. «Why is the Java main method static?». Получается, что java launcher (java.exe или javaw.exe) — это простое приложение (simple C application): оно загружает различные DLL, которые на самом деле являются JVM. Java launcher выполняет определённый набор Java Native Interface (JNI) вызовов. JNI — это механизм, соединяющий мир виртуальной машины Java и мир C++. Получается, что launcher — это не JVM, а её загрузчик. Он знает, какие правильные команды нужно выполнить, чтобы запустилась JVM. Знает, как организовать всё необходимое окружение при помощи JNI вызовов. В эту организацию окружения входит и создание главного потока, который обычно называется main . Чтобы нагляднее рассмотреть, какие живут потоки в java процессе, используем программу jvisualvm, которая входит в поставку JDK. Зная pid процесса, мы можем открыть данные сразу по нему: jvisualvm —openpid айдипроцесса Интересно, что каждый поток имеет свою обособленную область в памяти, выделенной для процесса. Эту структуру памяти называют стеком. Стек состоит из фрэймов. Фрэйм — это точка вызова метода, execution point. Также фрэйм может быть представлен как StackTraceElement (см. Java API для StackTraceElement). Подробнее про память, выделяемую каждому потоку, можно прочитать тут. Если посмотреть на Java API и поискать там слово Thread, мы увидим, что есть класс java.lang.Thread. Именно этот класс представляет в Java поток, и с ним нам и предстоит работать.
java.lang.Thread
- Не вызван метод Runtime.exit
- Все НЕ демон-потоки завершили свою работу (как без ошибок, так и с выбрасыванием исключений)
public static void main(String []args)
Группы позволяют упорядочить управление потоками и вести их учёт. Помимо групп, у потоков есть свой обработчик исключений. Взглянем на пример:
public static void main(String []args) < Thread th = Thread.currentThread(); th.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() < @Override public void uncaughtException(Thread t, Throwable e) < System.out.println("Возникла ошибка: " + e.getMessage()); >>); System.out.println(2/0); >
Деление на ноль вызовет ошибку, которая будет перехвачена обработчиком. Если обработчик не указывать самому, отработает реализация обработчика по умолчанию, которая будет в StdError выводить стэк ошибки. Подробнее можно прочитать в обзоре http://pro-java.ru/java-dlya-opytnyx/obrabotchik-neperexvachennyx-isklyuchenij-java/». Кроме того, у потока есть приоритет. Подробнее про приоритеты можно прочитать в статье «Java Thread Priority in Multithreading».
Создание потока
Как и сказано в документации, у нас 2 способа создать поток. Первый — создать своего наследника. Например:
public class HelloWorld < public static class MyThread extends Thread < @Override public void run() < System.out.println("Hello, World!"); >> public static void main(String []args) < Thread thread = new MyThread(); thread.start(); >>
Как видим, запуск задачи выполняется в методе run , а запуск потока в методе start . Не стоит их путать, т.к. если мы запустим метод run напрямую, никакой новый поток не будет запущен. Именно метод start просит JVM создать новый поток. Вариант с потомком от Thread плох уже тем, что мы в иерархию классов включаем Thread. Второй минус — мы начинаем нарушать принцип «Единственной ответственности» SOLID, т.к. наш класс становится одновременно ответственным и за управление потоком и за некоторую задачу, которая должна выполняться в этом потоке. Как же правильно? Ответ находится в том самом методе run , который мы переопределяем:
Здесь target — это некоторый java.lang.Runnable , который мы можем передать для Thread при создании экземпляра класса. Поэтому, мы можем сделать так:
public class HelloWorld < public static void main(String []args)< Runnable task = new Runnable() < public void run() < System.out.println("Hello, World!"); >>; Thread thread = new Thread(task); thread.start(); > >
А ещё Runnable является функциональным интерфейсом начиная с Java 1.8. Это позволяет писать код задач для потоков ещё красивее:
public static void main(String []args) < Runnable task = () ->< System.out.println("Hello, World!"); >; Thread thread = new Thread(task); thread.start(); >
Итого
Итак, надеюсь, из сего повестования понятно, что такое поток, как они существуют и какие базовые операции с ними можно выполнять. В следующей части стоит разобраться, как потоки взаимодействуют друг с другом и какой у них жизненный цикл. #Viacheslav
Многопоточность в Java
Прежде, чем узнать про потоки Java, давайте заглянем в недалёкое будущее. Представьте, что вы подали резюме и прошли собеседование. Вас и пару дюжин будущих коллег пригласили на работу в большую Software-компанию. Среди прочих хлопот нужно подать бумажные документы для трудоустройства уставшему сотруднику HR-отдела.
Чтобы ускорить процесс, претендентов на должность можно разделить на две группы и распределить их между двумя HR-менеджерами (если таковые есть в компании). В результате мы получаем ускорение процесса за счёт параллельной (parallel) работы по оформлению.
Если же кадровик в компании один, то придётся как-то выкручиваться. Например, можно снова- таки разбить всех на две группы, например, собеседовать поочерёдно девушек и юношей.
Или по другому принципу: так как в нижней группе больше народа, будем чередовать на одного юношу двух девушек.
Такой способ организации работы называется многопоточным. Наш утомлённый кадровик переключается на разные группы для оформления из них очередного сотрудника. Групп, может быть, одиннадцать, а кадровиков – четыре. В этом случае многопоточная (multithreading) обработка будет происходить параллельно несколькими HR-ами, которые могут брать очередного человека из любой группы для обработки его документов.
Процессы
Процессом (process) в данном случае будет организация работы приёма документов. В организации можно выделить несколько процессов: бухгалтерский учёт, разработка ПО, встречи с клиентами, работа склада и т. д. На каждый процесс выделены ресурсы: помещение, сотрудники для его исполнения. Процессы изолированы друг от друга: у кадровиков отсутствует доступ в бухгалтерскую базу, а менеджеры по работе с клиентами не бегают по складу. Если процесс должен получить доступ к чужим ресурсам, необходимо наладить межпроцессное взаимодействие: служебные записки, совместные совещания.
Потоки
Работа в процессе организована в виде потоков ( java thread ). Для отдела кадров, поток – это организация работы по обслуживанию группы. На самой первой картинке – один поток, последующих трёх – два. Внутри процесса потоки могут выполняться параллельно – два кадровика принимают две или более группы будущих сотрудников. Взаимодействие кадровиков с группами – обработку потоков внутри процесса – называют синхронизацией потоков. На рисунках оформления одним кадровиком двух групп видны показаны способы: равномерный (девушка – юноша – девушка – юноша) и с разными приоритетами (две девушки чередуются с одним юношей). Потоки имеют доступ к ресурсам процесса, к которому они относятся: группам к кадровику даны образцы бланков заявлений, ручки для заполнения документов. Но если потоки взаимодействуют с общими для них вещами – то возможны казусы. Если кадровик попросит крикнуть имя последнего человека в очереди – то, в случае с двумя группами, он не уверен заранее, что услышит женское имя или мужское. Подобные конфликты доступа к данным, блокировки и способы их разрешения – очень важная тема.
Состояния потока
- Создан ( New ) – очередь к кадровику готовится, люди организуются.
- Запущен ( Runnable ) – наша очередь выстроилась к кадровику и обрабатывается.
- Заблокирован ( Blocked ) – последний в очереди юноша пытается выкрикнуть имя, но услышав, что девушка в соседней группе начала делать это раньше него, замолчал.
- Завершён ( Terminated ) — вся очередь оформилась у кадровика и в ней нет необходимости.
- Ожидает( Waiting ) – одна очередь ждёт сигнала от другой.
Вернемся в IT-мир
В XXI веке многопоточное и параллельное выполнение стало актуальным. С 90-х годов прошлого века многозадачные операционные системы Windows, MacOS и Linux прочно обосновались на домашних компьютерах. В них часто можно встретить четырёх- и более ядерные процессоры. Число параллельных блоков GPU-видеокарт уже перевалило за тысячу. Популярные программы пишутся с учетом многопоточности (multithreading), например, современные версии ПО обработки графики, видео или оперирующих большим объемом данных: Adobe Photoshop, WinRar, Mathematica, современные игры. Многопоточность Java – очень важная, востребованная и сложная тема. Поэтому в курсе JavaRush встречается много задач, чтобы разобраться с ней очень хорошо. Java-примеры на многопоточность помогут освоить основные нюансы и тонкости этой области, синхронизации работы потоков.
Процесс
Process (процесс) – выполняющийся экземпляр программы, которому Операционная Система (ОС) выделила память, процессорное время/ядра и прочие ресурсы. Важно, что память выделяется отдельно, адресные пространства различных процессов недоступны друг другу. Если процессам необходимо обмениваться данными, они могут это сделать с помощью файлов, каналов и иных способов межпроцессного взаимодействия.
Поток
Java Thread (поток). Иногда, чтобы не путать с другими классами Java – Stream и подобными, потоки Java часто переводят как нить. Они используют выделенные для процесса ресурсы и являются способом выполнения процесса. Главный поток выполняет метод main и завершается. При выполнении процесса могут порождаться дополнительные потоки (дочерние). Потоки одного процесса могут между собой обмениваться данными. Многопоточность Java требует учитывать синхронизацию данных, не забывайте об этом. В Java процесс завершается тогда, когда закончил работу последний его поток. Для фоновых задач поток можно запустить как демон ( daemon ), отличие которого от обычного в том, что они будут принудительно завершены при окончании работы всех не- daemon потоков процесса.
Первое многопоточное приложение
Существует более полудюжины способов создания потоков, в рамках JavaRush курса мы их подробно разберём. Для начала познакомимся с одним из базовых. Имеется специальный класс Thread в методе run() которого необходимо написать код, реализующий логику программы. После создания потока, можно запустить его, вызвав метод start() . Напишем демонстрационную программу, реализующую пример многопоточности Java.
class PeopleQueue extends Thread/ Наша очередь из сотрудников, наследник класса Thread private String[] names; PeopleQueue(String. names) / Конструктор, аргумент- массив имен сотрудников this.names = names; > @Override public void run() < // Этот метод будет вызван при старте потока for (int i = 0; i < names.length; i++) < // Вывод в цикле с паузой 0.5 сек очередного сотрудника System.out.println("Обработаны документы: " + names[i]); try < sleep(500); // Задержка в 0.5 сек >catch (Exception e) <> > > > public class HR/ Класс для демонстрации работы потока public static void main(String[] args) < // Создаем две очереди PeopleQueue queue1 = new PeopleQueue("Иван","Сергей","Николай","Фердинанд","Василий"); PeopleQueue queue2 = new PeopleQueue("Мария","Людмила","Алиса","Карина","Ольга"); System.out.println("Начали!"); // Сообщение из главного потока программы queue1.start(); //Запускаем одну очередь (дочерний поток) queue2.start(); //Запускаем вторую (дочерний поток) > >
Запустим программу. В консоли виден вывод сообщения главным потоком. Далее, каждый дочерний поток queue1 и queue2 поочередно выводят сообщения в общую для них консоль об очередном обработанном сотруднике. Один из возможных вариантов работы программы:
Начали! Обработаны документы: Мария Обработаны документы: Иван Обработаны документы: Людмила Обработаны документы: Сергей Обработаны документы: Алиса Обработаны документы: Николай Обработаны документы: Карина Обработаны документы: Фердинанд Обработаны документы: Ольга Обработаны документы: Василий Process finished with exit code 0
Многопоточность в Java – тема трудная и многосторонняя. Умение писать код с использованием параллельных, многозадачных и многопоточных вычислений поможет вам эффективно реализовать задачи на современных многоядерных процессорах и кластерах, состоящих из множества компьютеров.