Жизненный цикл потока Java и переключение состояний

Java
Жизненный цикл потока Java и переключение состояний

помещение

Я был немного ленив в последнее время, и у меня не так много более подробных результатов. Просто захотелось перечитатьJUCИсходный код реализации пула потоков, давайте рассмотрим его подробнее перед этимJavaРеализация потока в , включая жизненный цикл потока, переключение состояния и переключение контекста потока и т. д. На момент написания этой статьи,JDKВерсия 11.

Реализация потоков Java

существуетПосле JDK1.2, модель многопоточности Java была определена на основе собственной модели многопоточности операционной системы. Таким образом, в текущей или будущих версиях JDK модель потоков, поддерживаемая операционной системой, в значительной степени определяет, как отображаются потоки виртуальной машины Java.Нет возможности достичь согласия по этому вопросу на разных платформах.В спецификации виртуальной машины Он также не определяет, с какой моделью многопоточности должны быть реализованы потоки Java. Модель потоков влияет только на масштаб параллелизма и стоимость операций потоков, и эти различия прозрачны для программ Java.

вести перепискуOracle Sun JDKИлиOracle Sun JVMЧто касается его версий для Windows и Linux, обе используютМодель потоковой передачи «один к одному»реализовано (как показано на рисунке ниже).

j-t-l-s-1.png

это одинJavaПоток сопоставляется с легковесным процессом (Light Weight Process), а облегченный поток сопоставляется с потоком ядра (Kernel-Level Thread). Поток, на который мы обычно ссылаемся, часто представляет собой легковесный процесс (или, вообще говоря, мы обычно создаем новый поток).java.lang.Threadявляется «дескриптором» легковесного экземпляра процесса, потому чтоjava.lang.Threadэкземпляр будет соответствоватьJVMтот внутриJavaThreadэкземпляр, в то время какJVMвнутриJavaThreadЭто следует понимать как легкий процесс). Из предыдущего расчета этого отношения сопоставления потоков мы можем узнать, что мы создаем или работаем в приложении.java.lang.ThreadЭкземпляры в конечном итоге будут сопоставлены с потоками ядра системы, если мы создадим злонамеренно или экспериментально бесконечноjava.lang.Threadнапример, в конечном итоге повлияет на нормальную работу системы или даже приведет к сбою системы (можно найти вWindowsПоэкспериментируйте в среде разработки, чтобы убедиться, что памяти достаточно для создания и запуска с бесконечным циклом.java.lang.Threadпример).

Существует два типа методов планирования потоков: совместное планирование потоков и вытесняющее планирование потоков.

планирование потоков описывать недостаток Преимущество
Совместное планирование потоков Время выполнения потока контролируется самим потоком.После завершения выполнения операционная система активно уведомляется о переключении на другой поток. Если поток не отдает время выполнения ЦП, это может привести к сбою всей системы. Простая реализация, отсутствие проблем с синхронизацией потоков
Упреждающее планирование потоков Время выполнения каждого потока распределяется операционной системой, и переключение потоков не определяется самим потоком Реализация относительно сложна, и операционная система должна управлять синхронизацией и переключением потоков. Это не будет блокировкой потока, вызывающей сбой системы

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

Многие операционные системы предоставляют концепцию приоритета потока, но из-за особенностей платформы приоритет потока в Java не соответствует приоритету системного потока на разных платформах, поэтому приоритет потока Java можно понимать только как "Приоритет рекомендации", с точки зрения непрофессионалаjava.lang.Thread#setPriority(int newPriority)не обязательно вступает в силу,Возможно, что приоритет Java-потока будет изменен самой системой.

Переключение состояний потоков Java

JavaСостояние потока может быть изменено сjava.lang.ThreadВнутренний класс перечисленияjava.lang.Thread$StateУзнайте, что:

public enum State {
      
    NEW,

    RUNNABLE,

    BLOCKED,

    WAITING,

    TIMED_WAITING,

    TERMINATED;
}

Описания этих состояний суммированы следующим образом:

j-t-l-s-3

Переключение отношений между состояниями потокаКартина выглядит следующим образом:

j-t-l-s-2

Давайте проанализируем значение состояния и переключение состояний потоков Java с помощью комментариев API и нескольких простых примеров кода.

НОВЫЙ статус

Примечания к API:

/**
 * Thread state for a thread which has not yet started.
 *
 */
NEW,

Экземпляр резьбы не был запущен, когда состояние потока.

Только что созданный, но еще не запущенный (еще не вызванныйThread#start()метод) экземпляра потока Java, который находится вNEWусловие.

public class ThreadState {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread();
        System.out.println(thread.getState());
    }
}

// 输出结果
NEW

ВЫПОЛНЯЕМОЕ состояние

Примечания к API:

/**
 * Thread state for a runnable thread.  A thread in the runnable
 * state is executing in the Java virtual machine but it may
 * be waiting for other resources from the operating system
 * such as processor.
 */
RUNNABLE,

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

Когда экземпляр потока Java вызываетThread#start()После этого вы войдетеRUNNABLEусловие.RUNNABLEСостояние можно рассматривать как содержащее два подсостояния:READYа такжеRUNNING.

  • READY: поток в этом состоянии может быть запланирован планировщиком потоков, чтобы изменить его наRUNNINGусловие.
  • RUNNING: это состояние указывает на то, что поток запущен, объект потокаrun()Инструкция, соответствующая коду в методе, выполняется процессором.

Когда экземпляр потока JavaThread#yield()Когда метод вызывается или из-за планирования планировщиком потока, состояние экземпляра потока может быть определено с помощьюRUNNINGВREADY, а из состояния потокаThread#getState()Полученный статус по-прежнемуRUNNABLE. Например:

public class ThreadState1 {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(()-> {
            while (true){
                Thread.yield();
            }
        });
        thread.start();
        Thread.sleep(2000);
        System.out.println(thread.getState());
    }
}
// 输出结果
RUNNABLE

ОЖИДАНИЕ статус

Примечания к API:

   /**
    * Thread state for a waiting thread.
    * A thread is in the waiting state due to calling one of the
    * following methods:
    * <ul>
    *   <li>{@link Object#wait() Object.wait} with no timeout</li>
    *   <li>{@link #join() Thread.join} with no timeout</li>
    *   <li>{@link LockSupport#park() LockSupport.park}</li>
    * </ul>
    *
    * <p>A thread in the waiting state is waiting for another thread to
    * perform a particular action.
    *
    * For example, a thread that has called {@code Object.wait()}
    * on an object is waiting for another thread to call
    * {@code Object.notify()} or {@code Object.notifyAll()} on
    * that object. A thread that has called {@code Thread.join()}
    * is waiting for a specified thread to terminate.
    */
    WAITING,

Состояние ожидающего потока. Поток входит в состояние ожидания, вызывая один из следующих методов: Object#wait() без тайм-аута Thread#join() без тайм-аута LockSupport.park() Поток в состоянии ожидания всегда ожидает, пока другой поток выполнит некоторую специальную обработку. Например: поток вызывает Object#wait(), затем он ожидает, пока другой поток вызовет Object#notify() или Object#notifyAll() для объекта; поток вызывает Thread#join(), затем он ожидает другой поток Поток завершается.

WAITINGданеопределенное состояние ожидания, потокам в этом состоянии не будет выделяться время выполнения ЦП. Когда поток выполняет определенные методы, он переходит в неопределенное состояние ожидания до тех пор, пока не будет явно разбужен.После пробуждения состояние потока изменяется наWAITINGизменить наRUNNABLEЗатем продолжайте.

RUNNABLEПеревести вWAITINGметод (ожидание бесконечно) WAITINGПеревести вRUNNABLEметод (пробуждение)
Object#wait() `Object#notify()
Thread#join() -
LockSupport.part() LockSupport.unpart(thread)

вThread#join()Этот метод относительно особенный. Он блокирует экземпляр потока до тех пор, пока экземпляр потока не будет выполнен. Вы можете наблюдать его исходный код следующим образом:

public final void join() throws InterruptedException {
    join(0);
}

public final synchronized void join(long millis)throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

видимыйThread#join()всегда вызывается, когда экземпляр потока живObject#wait()метод, то есть он должен выполняться после завершения потокаisAlive()Разблокируется только в случае false (это означает, что жизненный цикл потока закончился).

на основеWAITINGНапример статус:

public class ThreadState3 {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(()-> {
            LockSupport.park();
            while (true){
                Thread.yield();
            }
        });
        thread.start();
        Thread.sleep(50);
        System.out.println(thread.getState());
        LockSupport.unpark(thread);
        Thread.sleep(50);
        System.out.println(thread.getState());
    }
}
// 输出结果
WAITING
RUNNABLE

ВРЕМЕННОЕ ОЖИДАНИЕ

Примечания к API:

/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
*   <li>{@link #sleep Thread.sleep}</li>
*   <li>{@link Object#wait(long) Object.wait} with timeout</li>
*   <li>{@link #join(long) Thread.join} with timeout</li>
*   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
*   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,

Состояние ожидающего потока, определяющее конкретное время ожидания. Поток переходит в это состояние, вызывая один из следующих методов с заданным периодом ожидания: Thread.sleep() Object#wait() с тайм-аутом Thread#join() с тайм-аутом LockSupport.parkNanos() LockSupport.parkUntil()

TIMED WAITINGто естьограниченное состояние ожидания, это иWAITINGАналогично, потокам в этом состоянии не будет выделяться время выполнения ЦП, но потоки в этом состоянии не нужно явно пробуждать, им просто нужно дождаться истечения периода тайм-аута.VMПросыпайтесь, чем-то похоже на будильник в реальной жизни.

RUNNABLEПеревести вTIMED WAITINGметод (ограниченное ожидание) TIMED WAITINGПеревести вRUNNABLEметод (тайм-аут, чтобы освободить ожидание)
Object#wait(timeout) -
Thread#sleep(timeout) -
Thread#join(timeout) -
LockSupport.parkNanos(timeout) -
LockSupport.parkUntil(timeout) -

Например:

public class ThreadState4 {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(()-> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                //ignore
            }
        });
        thread.start();
        Thread.sleep(50);
        System.out.println(thread.getState());
        Thread.sleep(1000);
        System.out.println(thread.getState());
    }
}
// 输出结果
TIMED_WAITING
TERMINATED

ЗАБЛОКИРОВАНО

Примечания к API:

/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,

Это состояние указывает на то, что поток блокируется, ожидая получения блокировки монитора. Если поток заблокирован, поток ожидает входа в блокировку монитора синхронизированного блока или синхронизированного метода или повторного входа в синхронизированный блок или синхронизированный метод после вызова Object#wait().

BLOCKEDЭто состояние также является состоянием блокировки, и потокам в этом состоянии не будет выделяться время выполнения ЦП. Состояние потокаBLOCKEDКогда есть две возможные ситуации:

A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method

  1. Поток ожидает блокировки монитора и не может войти, пока не будет получена блокировка монитора.synchronizedкодовый блок илиsynchronizedметод, при котором блокируются все потоки процесса, ожидающие получения блокировки.

reenter a synchronized block/method after calling Object#wait()

  1. Проденьте X шагов вsynchronizedкодовый блок илиsynchronizedПосле вызова метода (в этот момент блокировка монитора была снята)Object#wait()После того, как метод заблокирован, когда принимающий другой поток T вызывает объект блокировкиObject#notify()/notifyAll(), но поток T еще не вышел изsynchronizedкодовый блок илиsynchronizedметод, то поток X по-прежнему заблокирован (обратите внимание наreenter, чтобы понять это, вдруг становится понятен Сценарий 2).

Для более подробного описания, пожалуйста, обратитесь к статье, которую я написал ранее:Глубокое понимание API блокировки и пробуждения, предоставляемого Object.

Вот простой пример для сценария 1 выше:

public class ThreadState6 {

    private static final Object MONITOR = new Object();

    public static void main(String[] args) throws Exception {
        Thread thread1 = new Thread(()-> {
            synchronized (MONITOR){
                try {
                    Thread.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    //ignore
                }
            }
        });
        Thread thread2 = new Thread(()-> {
            synchronized (MONITOR){
                System.out.println("thread2 got monitor lock...");
            }
        });
        thread1.start();
        Thread.sleep(50);
        thread2.start();
        Thread.sleep(50);
        System.out.println(thread2.getState());
    }
}
// 输出结果
BLOCKED

Для приведенной выше сцены 2 простой пример:

public class ThreadState7 {

    private static final Object MONITOR = new Object();
    private static final DateTimeFormatter F = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws Exception {
        System.out.println(String.format("[%s]-begin...", F.format(LocalDateTime.now())));
        Thread thread1 = new Thread(() -> {
            synchronized (MONITOR) {
                System.out.println(String.format("[%s]-thread1 got monitor lock...", F.format(LocalDateTime.now())));
                try {
                    Thread.sleep(1000);
                    MONITOR.wait();
                } catch (InterruptedException e) {
                    //ignore
                }
                System.out.println(String.format("[%s]-thread1 exit waiting...", F.format(LocalDateTime.now())));
            }
        });
        Thread thread2 = new Thread(() -> {
            synchronized (MONITOR) {
                System.out.println(String.format("[%s]-thread2 got monitor lock...", F.format(LocalDateTime.now())));
                try {
                    MONITOR.notify();
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    //ignore
                }
                System.out.println(String.format("[%s]-thread2 releases monitor lock...", F.format(LocalDateTime.now())));
            }
        });
        thread1.start();
        thread2.start();
        // 这里故意让主线程sleep 1500毫秒从而让thread2调用了Object#notify()并且尚未退出同步代码块,确保thread1调用了Object#wait()
        Thread.sleep(1500);  
        System.out.println(thread1.getState());
        System.out.println(String.format("[%s]-end...", F.format(LocalDateTime.now())));
    }
}
// 某个时刻的输出如下:
[2019-06-20 00:30:22]-begin...
[2019-06-20 00:30:22]-thread1 got monitor lock...
[2019-06-20 00:30:23]-thread2 got monitor lock...
BLOCKED
[2019-06-20 00:30:23]-end...
[2019-06-20 00:30:25]-thread2 releases monitor lock...
[2019-06-20 00:30:25]-thread1 exit waiting...

В сцене 2:

  • Звонки потока 2Object#notify()После бездействия в течение 2000 миллисекунд выйдите из блока кода синхронизации и снимите блокировку монитора.
  • Поток 1 спит только 1000 мс перед вызовомObject#wait(), в этот момент он снял блокировку монитора, поэтому поток 2 успешно входит в синхронизированный блок, а поток 1 находится в комментарии API, как указаноreenter a synchronized block/methodположение дел.
  • Основной поток спит в течение 1500 миллисекунд только для того, чтобы поразить поток 1 вreenterсостояние и вывести состояние своего потока, которое оказываетсяBLOCKEDусловие.

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

статус ЗАВЕРШЕН

Примечания к API:

/**
 * Thread state for a terminated thread.
 * The thread has completed execution.
 */ 
TERMINATED;

Конец потока, соответствующий состоянию потока, в котором поток завершился.

TERMINATEDСтатус указывает на то, что поток завершен. Экземпляр потока можно запустить только один раз, точнее, он будет вызван только один раз.Thread#run()метод,Thread#run()После завершения выполнения метода состояние потока изменяется наTERMINATED, что означает, что жизненный цикл потока закончился.

Возьмем простой пример:

public class ThreadState8 {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(() -> {

        });
        thread.start();
        Thread.sleep(50);
        System.out.println(thread.getState());
    }
}
// 输出结果
TERMINATED

переключатель контекста

В многопоточной среде, когда состояние потока определяетсяRUNNABLEпреобразовать в неRUNNABLE(BLOCKED,WAITINGилиTIMED_WAITING), контекстная информация соответствующего потока (также известная какContext,включатьCPUсодержимое регистров и программный счетчик в определенный момент времени и т. д.) необходимо сохранить, чтобы в дальнейшем поток мог возобновиться какRUNNABLEсостояние может продолжить выполнение на основе предыдущего хода выполнения. в то время как состояние потока определяется не-RUNNABLEГосударственная записьRUNNABLEВремя состояния может включать восстановление ранее сохраненной информации о контексте потока и продолжение выполнения на основе этого. Прямо здесьПроцесс сохранения и восстановления информации о контексте потоканазывается переключением контекста (Context Switch).

Контекстное переключение потоков приведет к дополнительной нагрузке на производительность, включая накладные расходы на сохранение и восстановление информации о контексте потока, планирование потоков.CPUзатраты времени иCPUНакладные расходы на аннулирование содержимого кэша (код, выполняемый потоком изCPUЗначение переменной, необходимое для доступа к нему из кеша, намного быстрее, чем доступ к нему из основной памяти (RAM) намного быстрее получить доступ к значению переменной ответа, ноПереключение контекста потока сделает недействительным содержимое кэша ЦП, к которому обращается соответствующий поток, обычно ЦП.L1 Cacheа такжеL2 Cache, так что соответствующий поток позже переназначается на среду выполнения, которая должна снова получить доступ к переменной в основной памяти для воссозданияCPUсодержимое кеша).

существуетLinuxсистема, черезvmstatКоманда для просмотра количества переключений глобального контекста, например:

$ vmstat 1

дляJavaпрограмма работает, вLinuxСистема также может бытьperfкоманда для мониторинга, например:

$ perf stat -e cpu-clock,task-clock,cs,cache-reference,cache-misses java YourJavaClass

упоминается в ссылкеWindowsВстроенные инструменты можно использовать под системойperfmon(по сути это диспетчер задач) для контроля переключения контекста потоков.На самом деле автор не нашел способа посмотреть переключение контекста из диспетчера задач.Поискав нашел инструмент:Process Explorer. бегатьProcess Explorerодновременно запуститьJavaпрограмму и проверьте ее статус:

j-t-l-s-4.png

Из-за точки останова вы можете видеть, что существует более 7000 переключений контекста работающей программы, а текущее приращение переключения контекста в одну секунду равно 26 (поскольку автор установилProcess ExplorerДанные обновляются каждую секунду).

Отслеживание состояния потока

Если проект запущен в продакшене, маловероятно, что он будет вызываться частоThread#getState()метод для отслеживания изменений состояния потока. Сам JDK предоставляет некоторые инструменты для мониторинга состояния потока, а также некоторые облегченные инструменты с открытым исходным кодом, такие как AliArthas, вот краткое введение.

использовать jvisualvm

jvisualvmЭто инструмент мониторинга индикатора JVM с ожиданием кучи и потока, который поставляется с JDK и подходит для использования в средах разработки и тестирования. Это расположеноJAVA_HOME/binПо каталогу.

j-t-l-s-5.png

в线程DumpКнопки аналогичны тем, которые будут упомянуты ниже.jstackКоманда для экспорта информации о стеке всех потоков.

использовать jstack

jstackЭто инструмент командной строки, поставляемый с JDK, его функция заключается в получении информации о стеке потоков процесса Java с указанным PID. например, один работает локальноIDEAпримерPID11376, то просто введите:

jstack 11376

Тогда вывод консоли будет следующим:

j-t-l-s-6.png

Кроме того, если вы хотите найти конкретный процесс JavaPID,можно использоватьjpsЗаказ.

Использование JMC

JMCто естьJava Mission Control, это также инструмент, поставляемый с JDK, и он предоставляет больше функций, чемjvisualvmМощный, включая обработку MBean, просмотр состояния стека потоков, регистратор полетов и многое другое.

j-t-l-s-7.png

резюме

Понимание переключения состояний потоков Java и некоторых методов мониторинга более полезно для ежедневной разработки многопоточных программ.В случае проблем в производственной среде можно быстро найти основную причину проблемы, отслеживая информацию о стеке потоков (обычно текущий мейнстримMVCприложение (точнее, оно должно бытьServletконтейнер, напримерTomcat) обрабатывать один запрос через поток. Когда запрос заблокирован, поток, который экспортирует соответствующий запрос на обработку, может в основном определить точное местоположение блокировки. Если используется очередь сообщений, напримерRabbitMQ, блокировка потребительского потока также может быть решена с помощью аналогичной идеи).

(Конец этой статьи e-a-20200804 c-3-d)