Потоки в Java: От рождения до смерти

Всем привет! В этой статье я бы хотел погрузить в мир жизненного цикла потоков начинающих программистов на Java, заранее извиняюсь за злоупотребление слова «поток» в этой статье, но я надеюсь так будет даже лучше для понимания. Поехали!

Жизненный цикл потока — основная концепция Java, которую мы подробно рассмотрим в этой статье. Мы будем использовать краткую иллюстрированную диаграмму и фрагменты практического кода, чтобы более глубоко понять состояния потока во время его выполнения. Эта статья о создании потока — отличное начало для понимания потоков в Java.

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

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

Состояния потока в Java

Состояния потока в Java

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

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

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

Первое состояние — »NEW ». Это состояние представляет собой недавно созданный поток, который еще не начал свое выполнение. В этом состоянии поток готов к планированию операционной системой, но ему не назначено никакого процессорного времени.

Следующее состояние — »RUNNABLE». Поток в этом состоянии либо запущен, либо готов к выполнению. Однако он может ожидать выделения ресурсов, таких как процессорное время или доступ к общему ресурсу. Как только необходимые ресурсы будут доступны, поток может продолжить свое выполнение.

Другое состояние »BLOCKED ». В этом состоянии поток ожидает получения блокировки монитора (monitor lock), которая необходима для ввода или повторного ввода синхронизированного блока или метода. Поток остается в этом состоянии до тех пор, пока monitor lock не станет доступен.

Ещё одно состояние — »WAITING». Когда поток находится в этом состоянии, он ожидает, пока какой-либо другой поток выполнит определенное действие без каких-либо временных ограничений. Это может произойти, например, когда один поток ожидает сигнала от другого для продолжения своего выполнения.

Понимая различные состояния потока, разработчики могут эффективно управлять выполнением своих многопоточных приложений. Это позволяет им оптимизировать использование ресурсов и эффективно обрабатывать синхронизацию, обеспечивая бесперебойную и результативную работу своих программ.
Чтобы полностью понять различные состояния потока, важно глубже вникнуть в каждое из них. Давайте начнем с состояния »TIMED_WAITING», он похож на »WAITING», но есть некоторые отличия. Это состояние возникает, когда один поток ожидает от другого выполнения определенного действия, но только в течение определенного периода времени.

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

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

Перейдём к примерам

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

Runnable runnable = new NewState();
Thread t = new Thread(runnable);
System.out.println(t.getState());

В данном случае метод t.getState () в консоль выведет »NEW»

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

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

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

Runnable runnable = new NewState();
Thread t = new Thread(runnable);
t.start();
System.out.println(t.getState());

Теперь метод t.getState () вероятнее всего в консоль выведет »RUNNABLE».
Почему вероятнее всего? Дело в том, что когда наш элемент управления достигнет t.getState (), мы не всегда можем быть уверены, что он будет находиться в состоянии RUNNABLE. Это связано с тем, что в некоторых случаях элемент может быть немедленно запланирован планировщиком потоков (Thread-Scheduler) и завершить своё выполнение. Именно в таких ситуациях возможны другие результаты.

Как мы уже выяснили, поток переходит в состояние Blocked, когда ожидает блокировки монитора (monitor lock) и пытается получить доступ к разделу кода, который заблокирован каким-либо другим потоком.

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

public class BlockedState {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new DemoBlockedRunnable());
        Thread t2 = new Thread(new DemoBlockedRunnable());
        
        t1.start();
        t2.start();
        
        Thread.sleep(1000);
        
        System.out.println(t2.getState());
        System.exit(0);
    }
}

class DemoBlockedRunnable implements Runnable {
    @Override
    public void run() {
        commonResource();
    }
    
    public static synchronized void commonResource() {
        while(true) {
          
        }
    }
}

Разбор кода:

  1. Мы создали два разных потока — t1 и t2

  2. t1 запускается и вводит синхронизированный метод commonResource (); это означает, что только один поток может получить к нему доступ; все остальные последующие потоки, которые попытаются получить доступ к этому методу, будут заблокированы от дальнейшего выполнения до тех пор, пока текущий не завершит обработку.

  3. Когда t1 входит в этот метод, он сохраняется в бесконечном цикле while; Это сделано для имитации интенсивной обработки, чтобы все остальные потоки не могли войти в этот метод.

  4. Теперь, когда мы запускаем t2, он пытается ввести метод commonResource (), к которому уже обращается t1, таким образом, t2 будет сохранен в состоянии BLOCKED.

    Вызовем t2.getState () и получим результат »BLOCKED»

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

1.object.wait ()

2.thread.join ()

3.LockSupport.park ()

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

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

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 DemoWaitingStateRunnable());
        t2.start();

        try {
            t2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }
}

class DemoWaitingStateRunnable implements Runnable {
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
        
        System.out.println(WaitingState.t1.getState());
    }
}

Разбор кода:

  1. Мы создали и запустили t1

  2. t1 создает t2 и запускает его

  3. Пока продолжается работа t2, мы вызываем t2.join (), это переводит t1 в состояние ожидания (WAITING), пока t2 не завершит выполнение.

  4. Поскольку t1 ожидает завершения t2, мы вызываем t1.getState () из t2 и получаем результат »WAITING»

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

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

  1. thread.sleep (long millis)

  2. wait (int timeout) or wait (int timeout, int nanos)

  3. thread.join (long millis)

  4. LockSupport.parkNanos

  5. LockSupport.parkUntil

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

public class TimedWaitingState {
    public static void main(String[] args) throws InterruptedException {
        DemoTimeWaitingRunnable runnable= new DemoTimeWaitingRunnable();
        Thread t1 = new Thread(runnable);
        t1.start();
        Thread.sleep(1000);
        System.out.println(t1.getState());
    }
}

class DemoTimeWaitingRunnable implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }
}

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

TERMINATED — это состояние мертвого потока. Поток находится в состоянии TERMINATED, когда он либо завершил выполнение, либо был как-то был прерван.

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

public class TerminatedState implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new TerminatedState());
        t1.start();
        Thread.sleep(1000);
        System.out.println(t1.getState());
    }
    
    @Override
    public void run() {
    }
}

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

Вывод

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

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

Желаю удачи в дальнейшем обучении!

© Habrahabr.ru