В Java потоки проходят четыре состояния в своём жизненном цикле:
New
Running
Waiting — [ Blocked, Waiting, TimedWaiting]
Dead / Terminated
![Жизненный цикл потока](https://media.proglib.io/posts/2022/11/16/90ad9c85eb69f556ab1fb79574be1ffa.png)
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. А затем выводит сообщение о завершении его выполнения. Если мы запустим эту программу, мы получим следующий вывод.
![☕🧵 Введение в многопоточность в Java. Часть 2. Жизненный цикл потоков, Thread.join() и потоки-демоны](https://media.proglib.io/posts/2022/11/16/b7384d2cc03d189936ec4e3c5f628bfe.png)
Как только 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
. Если мы запустим эту программу, мы получим следующий вывод.
![☕🧵 Введение в многопоточность в Java. Часть 2. Жизненный цикл потоков, Thread.join() и потоки-демоны](https://media.proglib.io/posts/2022/11/16/c47637d43e12affd9db555b117e36538.png)
Порядок вывода в этом примере изменился. Сразу после запуска 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 потоками ввода и вывода, отключает соединения с базами данных и т. д.
![☕🧵 Введение в многопоточность в Java. Часть 2. Жизненный цикл потоков, Thread.join() и потоки-демоны](https://media.proglib.io/posts/2022/11/16/04d842c725f28c3fd1ab2384f2b66fb7.png)
Потоки-демоны
в основном функционируют как вспомогательные потоки, они выполняют разные операции в фоновом режиме. Например, 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()
— возвращает имя текущего выполняющегося потока. При запуске данной программы, получим вывод:
![☕🧵 Введение в многопоточность в Java. Часть 2. Жизненный цикл потоков, Thread.join() и потоки-демоны](https://media.proglib.io/posts/2022/11/16/8176aa5b234b4cc33d6f25f4b97ca9b2.png)
Давайте определим наш собственный поток-демон. Мы создадим поток-демон, который будет печатать сообщение каждую секунду.
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
.
Если мы запустим программу, мы получим следующий вывод.
![☕🧵 Введение в многопоточность в Java. Часть 2. Жизненный цикл потоков, Thread.join() и потоки-демоны](https://media.proglib.io/posts/2022/11/16/5767e66218154699e309262aa8ee7399.png)
Кроме того, setDaemon(boolean)
можно вызывать только в том случае, если поток находится в состоянии New
и пока ещё не запущен, иначе мы получим исключение IllegalThreadStateException
.
В этой части статей о многопоточности мы обсудили жизненный цикл потока, метод Thread.join()
и потоки-демоны. В следующей статье обсудим синхронизацию потоков, что такое монитор объекта и взаимодействие между потоками.
Комментарии