Перейти к основному содержимому

Жизненный цикл потока в Java

· 7 мин. чтения

1. Введение

В этой статье мы подробно обсудим основную концепцию Java — жизненный цикл потока.

Мы будем использовать краткую иллюстрированную диаграмму и, конечно же, практические фрагменты кода, чтобы лучше понять эти состояния во время выполнения потока.

Чтобы начать понимать потоки в Java, эта статья о создании потока является хорошей отправной точкой.

2. Многопоточность в Java

В языке Java многопоточность управляется основной концепцией Thread . В течение своего жизненного цикла потоки проходят через различные состояния:

./366a3eedcf2109e3802211b0cad373ce.jpg

3. Жизненный цикл потока в Java

Класс java.lang.Thread содержит статическое перечисление состояний , которое определяет его потенциальные состояния. В любой момент времени поток может находиться только в одном из следующих состояний:

  1. NEW — вновь созданный поток, который еще не начал выполнение
  2. RUNNABLE — либо запущен, либо готов к выполнению, но ожидает выделения ресурсов
  3. ЗАБЛОКИРОВАНО — ожидание получения блокировки монитора для входа или повторного входа в синхронизированный блок/метод
  4. WAITING — ожидание выполнения какого-либо другого потока определенного действия без ограничения времени.
  5. TIMED_WAITING — ожидание того, что какой-то другой поток выполнит определенное действие в течение указанного периода.
  6. TERMINATED – завершил выполнение

Все эти состояния показаны на диаграмме выше; давайте теперь обсудим каждый из них подробно.

3.1. Новый

НОВАЯ нить (или рожденная нить ) — это нить, которая была создана, но еще не запущена. Он остается в этом состоянии до тех пор, пока мы не запустим его с помощью метода start() .

В следующем фрагменте кода показан только что созданный поток, находящийся в состоянии NEW :

Runnable runnable = new NewState();
Thread t = new Thread(runnable);
Log.info(t.getState());

Поскольку мы не запустили указанный поток, метод t.getState() выводит:

NEW

3.2. Запускаемый

Когда мы создали новый поток и вызвали для него метод start() , он перешел из состояния NEW в состояние RUNNABLE . Потоки в этом состоянии либо запущены, либо готовы к запуску, но ожидают выделения ресурсов системой.

В многопоточной среде планировщик потоков (который является частью JVM) выделяет фиксированное количество времени для каждого потока. Таким образом, он выполняется определенное время, а затем передает управление другим RUNNABLE потокам.

Например, давайте добавим метод t.start() в наш предыдущий код и попробуем получить доступ к его текущему состоянию:

Runnable runnable = new NewState();
Thread t = new Thread(runnable);
t.start();
Log.info(t.getState());

Этот код, скорее всего , вернет вывод как:

RUNNABLE

Обратите внимание, что в этом примере не всегда гарантируется, что к тому времени, когда наш элемент управления достигнет t.getState() , он все еще будет находиться в состоянии RUNNABLE .

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

3.3. Заблокировано

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

Попробуем воспроизвести это состояние:

public class BlockedState {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new DemoThreadB());
Thread t2 = new Thread(new DemoThreadB());

t1.start();
t2.start();

Thread.sleep(1000);

Log.info(t2.getState());
System.exit(0);
}
}

class DemoThreadB implements Runnable {
@Override
public void run() {
commonResource();
}

public static synchronized void commonResource() {
while(true) {
// Infinite loop to mimic heavy processing
// 't1' won't leave this method
// when 't2' try to enter this
}
}
}

В этом коде:

  1. Мы создали два разных потока — t1 и t2.
  2. t1 запускает и входит в синхронизированный метод commonResource() ; это означает, что к нему может получить доступ только один поток; все остальные последующие потоки, пытающиеся получить доступ к этому методу, будут заблокированы от дальнейшего выполнения до тех пор, пока текущий не завершит обработку
  3. Когда t1 входит в этот метод, он сохраняется в бесконечном цикле while; это просто для имитации тяжелой обработки, чтобы все другие потоки не могли войти в этот метод
  4. Теперь, когда мы запускаем t2 , он пытается войти в метод commonResource() , к которому уже обращается t1, таким образом, t2 будет оставаться в состоянии BLOCKED .

Находясь в этом состоянии, мы вызываем t2.getState() и получаем результат в виде:

BLOCKED

3.4. Ожидающий

Поток находится в состоянии WAITING , когда он ожидает, пока другой поток выполнит определенное действие. Согласно JavaDocs , любой поток может войти в это состояние, вызвав любой из следующих трех методов:

  1. объект.ждите()
  2. thread.join() или
  3. LockSupport.park()

Обратите внимание, что в функциях wait() и join() мы не определяем период ожидания, поскольку этот сценарий рассматривается в следующем разделе.

У нас есть отдельный учебник , в котором подробно обсуждается использование wait() , notify( ) и notifyAll() .

А пока попробуем воспроизвести это состояние:

public class WaitingState implements Runnable {
public static Thread t1;

public static void main(String[] args) {
t1 = new Thread(new WaitingState());
t1.start();
}

public void run() {
Thread t2 = new Thread(new DemoThreadWS());
t2.start();

try {
t2.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
}

class DemoThreadWS implements Runnable {
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}

Log.info(WaitingState.t1.getState());
}
}

Давайте обсудим, что мы здесь делаем:

  1. Мы создали и запустили t1
  2. t1 создает t2 и запускает его
  3. Пока обработка t2 продолжается, мы вызываем t2.join() , это переводит t1 в состояние WAITING до тех пор, пока t2 не завершит выполнение.
  4. Поскольку t1 ожидает завершения t2 , мы вызываем t1.getState() из t2 .

Вывод здесь, как и следовало ожидать:

WAITING

3.5. Ожидание по времени

Поток находится в состоянии TIMED_WAITING , когда он ожидает, пока другой поток выполнит определенное действие в течение заданного периода времени.

Согласно JavaDocs , существует пять способов перевести поток в состояние TIMED_WAITING :

  1. thread.sleep (длинный миллис)
  2. ожидание (целое время ожидания) или ожидание (целое время ожидания, целое число nanos)
  3. thread.join (длинный миллис )
  4. LockSupport.parkNanos
  5. LockSupport.parkUntil

Чтобы узнать больше о различиях между wait() и sleep() в Java, ознакомьтесь с этой специальной статьей здесь .

А пока попробуем быстро воспроизвести это состояние:

public class TimedWaitingState {
public static void main(String[] args) throws InterruptedException {
DemoThread obj1 = new DemoThread();
Thread t1 = new Thread(obj1);
t1.start();

// The following sleep will give enough time for ThreadScheduler
// to start processing of thread t1
Thread.sleep(1000);
Log.info(t1.getState());
}
}

class DemoThread implements Runnable {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
}

Здесь мы создали и запустили поток t1 , который переходит в спящее состояние с периодом ожидания 5 секунд; вывод будет:

TIMED_WAITING

3.6. Прекращено

Это состояние мертвого потока. Он находится в состоянии TERMINATED , когда либо завершил выполнение, либо был аварийно завершен.

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

Попробуем достичь этого состояния на следующем примере:

public class TerminatedState implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new TerminatedState());
t1.start();
// The following sleep method will give enough time for
// thread t1 to complete
Thread.sleep(1000);
Log.info(t1.getState());
}

@Override
public void run() {
// No processing in this block
}
}

Здесь, когда мы запустили поток t1 , следующий оператор Thread.sleep(1000) дает достаточно времени для завершения t1 , поэтому эта программа выдает нам результат:

TERMINATED

В дополнение к состоянию потока мы можем проверить метод isAlive() , чтобы определить, активен поток или нет. Например, если мы вызовем метод isAlive() в этом потоке:

Assert.assertFalse(t1.isAlive());

Он возвращает ложь. Проще говоря, поток жив тогда и только тогда, когда он был запущен и еще не умер.

4. Вывод

В этом руководстве мы узнали о жизненном цикле потока в Java. Мы рассмотрели все шесть состояний, определенных перечислением Thread.State , и воспроизвели их на кратких примерах.

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

И, как всегда, используемые здесь фрагменты кода доступны на GitHub .