«Гораздо проще уже спроектировать класс потокобезопасным, чем модернизировать его позже».
― Брайан Гетц.
Потоки 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
Когда поток находится в состоянии 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. Остается только немного попрактиковаться.
Читайте также:
- Новый подход к пониманию RxJava
- Избегаем исключения Null Pointer Exception в Java с помощью Optional
- Графовое моделирование данных на Java
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Ravidu Perera: States of Thread in Java