В этой части узнаем, какие состояния проходят потоки в своем жизненном цикле, что такое ожидание потоков и что такое потоки-демоны. ← Часть 1 Преимущества и недостатки многопоточности В Java потоки проходят четыре состояния в своём жизненном цикле: New Running Waiting — [ Blocked, Waiting, TimedWaiting] Dead / Terminated Жизненный цикл потока New — состояние, когда поток создан и готов к использованию. Это состояние, когда мы еще не запустили поток. Running — состояние, в которое поток переходит после того, как мы его запустили. В этом состоянии поток выполняет свою работу, т. е. логику, которую мы определили. Waiting — состояние ожидания, в которое поток может перейти во время своего выполнения. Есть три состояний ожидания — Blocked, Waiting, TimedWaiting. Например, когда поток пытается получить монитор объекта, он входит в состояние блокировки — Blocked; когда поток ожидает выполнения другого потока, тогда поток переходит в состояние ожидания — Waiting, а когда поток ожидает только определённое количество времени для выполнения другого потока, поток входит в состояние — TimedWaiting. Поток возвращается в состояние — Running, как только другие потоки выполнили или освободили монитор объекта. Поток может бесконечно менять свое состояние из состояния — Running в состояние — Waiting и наоборот. Dead / Terminated — состояние, в которое поток переходит после завершения выполнения или в случае возникновения исключений. Поток после выполнения не может быть запущен снова. Если мы попытаемся запустить поток в состоянии — Dead / Terminated, мы получим исключение IllegalStateException. Thread join() Еще один полезный метод, предоставляемый классом Thread — join(). При написании многопоточных программ могут быть случаи, когда необходимо ждать завершения выполнения какого-либо потока и после этого продолжить выполнение текущего потока. В таких случаях полезно применять метод — join(). Данный метод позволяет одному потоку ожидать завершения другого. Рассмотрим следующий пример: public class App { public static void main(String[] args) { var threadTwo = new Thread(() -> { try { Thread.sleep(2000); int counter = 0; for (int i = 0; i < 1000; i++) { counter ++; } var thread = Thread.currentThread().getName(); System.out.println(thread + " has finished its execution, counter = " + counter); } catch (InterruptedException exception) { exception.printStackTrace(); } }, "Counter thread"); threadTwo.start(); System.out.println("Main method executing"); } } В этом примере мы создаем поток — threadTwo, который ждёт две секунды и считает до 1000. А затем выводит сообщение о завершении его выполнения. Если мы запустим эту программу, мы получим следующий вывод. Как только threadTwo начинает выполняться, метод main() продолжает свое выполнение, он печатает — «Main method executing» и завершает свое выполнение. Параллельно выполняется threadTwo, считает до 1000, потом выводит — «Counter has finished its execution, counter = 1000» и заканчивает выполнение. Но что, если мы хотим, чтобы основной поток ждал завершения выполнения потока — threadTwo? Как этого добиться? Довольно просто — использование join() делает то, что нужно. Рассмотрим следующий пример: public class App { public static void main(String[] args) throws InterruptedException { var threadTwo = new Thread(() -> { try { Thread.sleep(2000); int counter = 0; for (int i = 0; i < 1000; i++) { counter ++; } var thread = Thread.currentThread().getName(); System.out.println(thread + " has finished its execution, counter = " + counter); } catch (InterruptedException exception) { exception.printStackTrace(); } }, "Counter thread"); threadTwo.start(); threadTwo.join(); System.out.println("Main method executing"); } } Данный пример почти то же самое с предыдущим примером, но с небольшой разницей. Сразу после threadTwo.start() мы добавили вызов метода — join() у потока threadTwo. Если мы запустим эту программу, мы получим следующий вывод. Порядок вывода в этом примере изменился. Сразу после запуска threadTwo, основной поток вызывает метод join() у потока — threadTwo. Это приводит к тому, что основной поток переходит в состояние ожидания и ждет, пока threadTwo не завершит свое выполнение. Как видно из вывода, поток — threadTwo завершает выполнение, считает до 1000 и выводит сообщение — «Counter has finished its execution, counter = 1000» и заканчивает выполнение. После этого mainThread продолжает свое выполнение и выводит следующее сообщение — «Main method executing». Вдобавок, если во время выполнения потока — threadTwo возникнет исключение, основной поток продолжит свое выполнение аналогично как в случае с успешным выполнением потока — threadTwo, ситуаций deadlock не будет. Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека джависта» Интересно, перейти к каналу Daemon Threads В Java существует два типа потоков — пользовательские (те, которые мы создаем) потоки и потоки-демоны. Когда запускается Java-программа, сразу же начинается выполняться один поток — основной поток. Основной поток запускает метод main(). Мы можем создавать новые потоки из основного потока. Основной поток завершает выполнение последним, поскольку он выполняет различные операции завершения работы c потоками ввода и вывода, отключает соединения с базами данных и т. д. Потоки-демоны в основном функционируют как вспомогательные потоки, они выполняют разные операции в фоновом режиме. Например, Garbage Collection в Java выполняется в фоновом режиме как поток-демон. В основном потоке мы можем создавать столько потоков, сколько необходимо. Более того, класс Thread предоставляет метод setDaemon(boolean), который позволяет рабочему (=пользовательскому) потоку превратиться в поток-демон. Потоки-демоны: поток с низким приоритетом, работающий в фоновом режиме; поток-демон автоматически завершается виртуальной машиной Java, когда все остальные рабочие (worker threads) потоки завершают выполнение; обычно потоки-демоны используются для операций ввода-вывода и сервисов (в смартфонах для связи Bluetooth или NFC). Основное различие между пользовательским потоком и потоком-демон заключается в том, что когда все рабочие (=основные или пользовательские) потоки завершают выполнение или умирают, потоки-демоны автоматически завершаются JVM, даже если они все еще выполняются. Thread.getName() public class App { public static void main(String[] args) { var threadName = Thread.currentThread().getName(); System.out.println("Thread that is executing: " + threadName); } } Пример, приведенный выше, иллюстрирует использование метода — Thread.getName(). Thread.getName() — возвращает имя текущего выполняющегося потока. При запуске данной программы, получим вывод: Давайте определим наш собственный поток-демон. Мы создадим поток-демон, который будет печатать сообщение каждую секунду. public class App { public static void main(String[] args) { var worker = new Thread(() -> { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } var threadName = Thread.currentThread().getName(); System.out.println("Thread is finishing its execution with name: " + threadName); }, "Worker"); var daemon = new Thread(() -> { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } var threadName = Thread.currentThread().getName(); System.out.println("Thread is executing with name: " + threadName); } }, "Daemon"); daemon.setDaemon(true); worker.start(); daemon.start(); var threadName = Thread.currentThread().getName(); System.out.println("Thread is executing with name: " + threadName); } } В приведенном выше примере мы создаем два потока и называем их Worker и Daemon. Поток — worker ожидает три секунды, затем печатает сообщение — «Thread is finishing its execution with name: Worker», а затем завершает выполнение. Поток daemon выполняется в цикле, каждую секунду печатает сообщение «Thread is executing with name: Daemon» и никогда не выходит из цикла. Для потока daemon инициализируем флаг daemonFlag, как true — daemon.setDaemon(true). Поток daemon будет работать бесконечно, пока другие потоки не завершат выполнения. После запуска потоков worker и daemon, основной поток продолжает выполнение — выводит сообщение «Thread is executing with name: main» и завершает выполнение. Как только поток worker завершит свое выполнение, рабочих потоков не останется, и поток-демон будет завершен JVM. Если мы запустим программу, мы получим следующий вывод. Кроме того, setDaemon(boolean) можно вызывать только в том случае, если поток находится в состоянии New и пока ещё не запущен, иначе мы получим исключение IllegalThreadStateException. В этой части статей о многопоточности мы обсудили жизненный цикл потока, метод Thread.join() и потоки-демоны. В следующей статье обсудим синхронизацию потоков, что такое монитор объекта и взаимодействие между потоками. *** Материалы по теме ☕ Сертификаты и тренинги для Java-разработчика 🗣 Путь в Java, или Зачем нужен ментор: интервью ☕ Учебник по Java: инкапсуляция на простых примерах
← Часть 1 Преимущества и недостатки многопоточности
В Java потоки проходят четыре состояния в своём жизненном цикле:
New
Running
Waiting — [ Blocked, Waiting, TimedWaiting]
Dead / Terminated
Жизненный цикл потока
New — состояние, когда поток создан и готов к использованию. Это состояние, когда мы еще не запустили поток.
Running — состояние, в которое поток переходит после того, как мы его запустили. В этом состоянии поток выполняет свою работу, т. е. логику, которую мы определили.
Waiting — состояние ожидания, в которое поток может перейти во время своего выполнения. Есть три состояний ожидания — Blocked, Waiting, TimedWaiting. Например, когда поток пытается получить монитор объекта, он входит в состояние блокировки — Blocked; когда поток ожидает выполнения другого потока, тогда поток переходит в состояние ожидания — Waiting, а когда поток ожидает только определённое количество времени для выполнения другого потока, поток входит в состояние — TimedWaiting. Поток возвращается в состояние — Running, как только другие потоки выполнили или освободили монитор объекта. Поток может бесконечно менять свое состояние из состояния — Running в состояние — Waiting и наоборот.
Waiting
Blocked
TimedWaiting
Dead / Terminated — состояние, в которое поток переходит после завершения выполнения или в случае возникновения исключений. Поток после выполнения не может быть запущен снова. Если мы попытаемся запустить поток в состоянии — Dead / Terminated, мы получим исключение IllegalStateException.
IllegalStateException
Еще один полезный метод, предоставляемый классом Thread — join(). При написании многопоточных программ могут быть случаи, когда необходимо ждать завершения выполнения какого-либо потока и после этого продолжить выполнение текущего потока. В таких случаях полезно применять метод — join(). Данный метод позволяет одному потоку ожидать завершения другого.
Thread
join()
Рассмотрим следующий пример:
public class App { public static void main(String[] args) { var threadTwo = new Thread(() -> { try { Thread.sleep(2000); int counter = 0; for (int i = 0; i < 1000; i++) { counter ++; } var thread = Thread.currentThread().getName(); System.out.println(thread + " has finished its execution, counter = " + counter); } catch (InterruptedException exception) { exception.printStackTrace(); } }, "Counter thread"); threadTwo.start(); System.out.println("Main method executing"); } }
В этом примере мы создаем поток — threadTwo, который ждёт две секунды и считает до 1000. А затем выводит сообщение о завершении его выполнения. Если мы запустим эту программу, мы получим следующий вывод.
threadTwo
Как только threadTwo начинает выполняться, метод main() продолжает свое выполнение, он печатает — «Main method executing» и завершает свое выполнение. Параллельно выполняется threadTwo, считает до 1000, потом выводит — «Counter has finished its execution, counter = 1000» и заканчивает выполнение.
main()
Но что, если мы хотим, чтобы основной поток ждал завершения выполнения потока — threadTwo? Как этого добиться? Довольно просто — использование join() делает то, что нужно.
public class App { public static void main(String[] args) throws InterruptedException { var threadTwo = new Thread(() -> { try { Thread.sleep(2000); int counter = 0; for (int i = 0; i < 1000; i++) { counter ++; } var thread = Thread.currentThread().getName(); System.out.println(thread + " has finished its execution, counter = " + counter); } catch (InterruptedException exception) { exception.printStackTrace(); } }, "Counter thread"); threadTwo.start(); threadTwo.join(); System.out.println("Main method executing"); } }
Данный пример почти то же самое с предыдущим примером, но с небольшой разницей. Сразу после threadTwo.start() мы добавили вызов метода — join() у потока threadTwo. Если мы запустим эту программу, мы получим следующий вывод.
threadTwo.start()
Порядок вывода в этом примере изменился. Сразу после запуска threadTwo, основной поток вызывает метод join() у потока — threadTwo. Это приводит к тому, что основной поток переходит в состояние ожидания и ждет, пока threadTwo не завершит свое выполнение. Как видно из вывода, поток — threadTwo завершает выполнение, считает до 1000 и выводит сообщение — «Counter has finished its execution, counter = 1000» и заканчивает выполнение. После этого mainThread продолжает свое выполнение и выводит следующее сообщение — «Main method executing».
Вдобавок, если во время выполнения потока — threadTwo возникнет исключение, основной поток продолжит свое выполнение аналогично как в случае с успешным выполнением потока — threadTwo, ситуаций deadlock не будет.
deadlock
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека джависта» Интересно, перейти к каналу
В Java существует два типа потоков — пользовательские (те, которые мы создаем) потоки и потоки-демоны. Когда запускается Java-программа, сразу же начинается выполняться один поток — основной поток. Основной поток запускает метод main(). Мы можем создавать новые потоки из основного потока. Основной поток завершает выполнение последним, поскольку он выполняет различные операции завершения работы c потоками ввода и вывода, отключает соединения с базами данных и т. д.
потоки-демоны
Потоки-демоны в основном функционируют как вспомогательные потоки, они выполняют разные операции в фоновом режиме. Например, Garbage Collection в Java выполняется в фоновом режиме как поток-демон.
Потоки-демоны
Garbage Collection
поток-демон
В основном потоке мы можем создавать столько потоков, сколько необходимо. Более того, класс Thread предоставляет метод setDaemon(boolean), который позволяет рабочему (=пользовательскому) потоку превратиться в поток-демон.
setDaemon(boolean)
Потоки-демоны:
Основное различие между пользовательским потоком и потоком-демон заключается в том, что когда все рабочие (=основные или пользовательские) потоки завершают выполнение или умирают, потоки-демоны автоматически завершаются JVM, даже если они все еще выполняются.
public class App { public static void main(String[] args) { var threadName = Thread.currentThread().getName(); System.out.println("Thread that is executing: " + threadName); } }
Пример, приведенный выше, иллюстрирует использование метода — Thread.getName(). Thread.getName() — возвращает имя текущего выполняющегося потока. При запуске данной программы, получим вывод:
Thread.getName()
Давайте определим наш собственный поток-демон. Мы создадим поток-демон, который будет печатать сообщение каждую секунду.
public class App { public static void main(String[] args) { var worker = new Thread(() -> { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } var threadName = Thread.currentThread().getName(); System.out.println("Thread is finishing its execution with name: " + threadName); }, "Worker"); var daemon = new Thread(() -> { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } var threadName = Thread.currentThread().getName(); System.out.println("Thread is executing with name: " + threadName); } }, "Daemon"); daemon.setDaemon(true); worker.start(); daemon.start(); var threadName = Thread.currentThread().getName(); System.out.println("Thread is executing with name: " + threadName); } }
В приведенном выше примере мы создаем два потока и называем их Worker и Daemon. Поток — worker ожидает три секунды, затем печатает сообщение — «Thread is finishing its execution with name: Worker», а затем завершает выполнение.
Worker
Daemon
worker
Поток daemon выполняется в цикле, каждую секунду печатает сообщение «Thread is executing with name: Daemon» и никогда не выходит из цикла. Для потока daemon инициализируем флаг daemonFlag, как true — daemon.setDaemon(true). Поток daemon будет работать бесконечно, пока другие потоки не завершат выполнения.
daemon
daemonFlag
daemon.setDaemon(true)
После запуска потоков worker и daemon, основной поток продолжает выполнение — выводит сообщение «Thread is executing with name: main» и завершает выполнение.
Как только поток worker завершит свое выполнение, рабочих потоков не останется, и поток-демон будет завершен JVM.
JVM
Если мы запустим программу, мы получим следующий вывод.
Кроме того, setDaemon(boolean) можно вызывать только в том случае, если поток находится в состоянии New и пока ещё не запущен, иначе мы получим исключение IllegalThreadStateException.
IllegalThreadStateException
В этой части статей о многопоточности мы обсудили жизненный цикл потока, метод Thread.join() и потоки-демоны. В следующей статье обсудим синхронизацию потоков, что такое монитор объекта и взаимодействие между потоками.
Thread.join()
***
Ваш адрес email не будет опубликован. Обязательные поля помечены *
Сохранить моё имя, email и адрес сайта в этом браузере для последующих моих комментариев.
Δ
Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.