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

«Гораздо проще уже спроектировать класс потокобезопасным, чем модернизировать его позже».
― Брайан Гетц.

Состояния потоков (codeGym)

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

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

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

1. New

Когда создается новый поток, он находится в состоянии New, причем запускаться поток еще не начал. Не начал запускаться его код, ему еще предстоит выполниться.

Пример: новый поток создается, но не запускается, поэтому остается вот таким:

Thread newThread = new newThreadClass();

Объект Thread пуст, и ресурсы для потока недоступны. Если пользователь вызовет какой-то другой метод, кроме start(), произойдет ошибка IllegalThreadStateExecption.

2. Runnable

Поток, готовый к запуску, переходит в состояние runnable.

Thread newThread = new newThreadClass();
newThread.start();

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

3. Blocked

При попытке выполнить задачу, которая не может быть завершена в данный момент времени, поток из состояния runnable переходит в состояние blocked. И ждет, пока задача не будет завершена.

Например, когда поток ожидает завершения операций ввода-вывода, он находится в состоянии blocked. Поток в этом состоянии не может дальше продолжать выполнение до тех пор, пока не перейдет в состояние runnable. Планировщик потоков повторно активирует blocked/waiting (блокированный/ожидающий) поток и планирует его выполнение. Любой поток, находясь в одном из этих состояний, не потребляет процессорное время.

4. Waiting

Ожидающий поток (FastThread)

Когда поток находится в состоянии waiting, он ждет другой поток, связанный условием. Когда это условие выполняется, планировщик получает уведомление и вызываются методы notify () или notifyAll(). В этом случае ожидающий поток переходит в состояние runnable.

Если выполняемый в это время поток переходит в состояние blocked/waiting, планировщик дает добро на выполнение ожидающего потока, перешедшего в состояние runnable. Именно планировщик потоков определяет, какой поток должен выполняться.

5. Time waiting

Поток находится в состоянии runnable. Теперь он вызывает метод sleep(t), wait(t) или join(t) с неким промежутком времени в качестве параметра и переходит в состояние time waiting. Поток остается в этом состоянии до тех пор, пока время ожидания не выйдет или пока не будет получено уведомление. Например, когда поток вызывает sleep или условное ожидание, он переходит в состояние timed waiting (ожидание с ограничением по времени). Как только время выйдет, поток вернется в состояние runnable.

6. Terminate

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

· Поток завершается в обычном режиме, когда код потока полностью выполнен программой.

· При выполнении потока произошло какое-то нештатное событие, сопровождаемое появлением ошибки, например ошибки сегментации или необработанного исключения.

Планирование потоков

Говоря о потоках и состояниях потоков в Java, не стоит забывать о планировании потоков.

Планирование потоков применяется для определения приоритета потоков. При запуске потока ему достается определенный приоритет: как максимум MAX_PRIORITY= 10 и как минимум MIN_PRIORITY = 1.

Обычный приоритет будет равен пяти (NORMAL_PRIORITY = 5).

Согласно правилу планирования потоков, добро на выполнение всегда дается потоку с более высоким приоритетом, а поток с низким уровнем приоритета переходит в состояние waiting.

Если потоки имеют равный приоритет, то они будут выполняться согласно методу Round-Robin, т. е. перебором по круговому циклу.

class Racer extends Thread
{
  Racer(int id){
  super( "Racer[" + id + "]" ) ;
}

public void run()
{
 for ( int i = 1 ; i < 40 ; i++ ) {
   if ( i % 10 == 0 ) {
   System.out.println( getName() + ", i = " + i ) ;
   yield() ;
 }
}
}
} // Racer

class RaceStarter
{
 public static void main( String args[] )
{

Racer[] racer = new Racer[4] ;
 for ( int i = 0 ; i < 4 ; i++ ) {
racer[i] = new Racer(i) ;

 }

racer[0].setPriority(7) ;
racer[1].setPriority(7) ;
racer[3].setPriority(3) ;

for ( int i = 0 ; i < 4 ; i++ ) {
racer[i].start() ;
}
}
 } // RaceStarter

В этом сценарии racer[0] и racer[1] имеют значение приоритета 7, а у racer[3] оно равно 3.

То есть racer[0] и racer[1] будут выполняться как первый поток, так как у них самый высокий приоритет среди потоков в состоянии RUNNABLE.

Что касается метода Round-Robin: если racer[0] будет выбран первым на выполнение, то другие будут ожидать его завершения. После чего начнет выполняться racer[1]. Они будут выполняться до тех пор, пока их процесс не будет завершен. И только затем запустится поток racer[3], имеющий самый низкий приоритет. Он будет выполняться без каких-либо помех до завершения своего процесса.

Надеюсь, вы получили четкое представление о состояниях и планировании потоков в Java. Остается только немного попрактиковаться.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Ravidu Perera: States of Thread in Java

Предыдущая статьяКраткая история инструментов веб-дизайна
Следующая статьяИзысканные уловки хакеров для кражи подарочных карт