Практически любое техническое интервью по Java содержит несколько вопросов, охватывающих темы параллелизма Java. Обычный сценарий для звука начального и среднего уровня звучит так:
Интервьюер: Можете ли вы перечислить состояния жизненного цикла потока Java?
Кандидат: Да, конечно! Итак, мы имеем: NEW
, RUNNABLE
, BLOCKED
, WAITING
, TIMED_WAITING
, и TERMINATED
.
Опрашивающий: Хорошо! Теперь, вы можете написать фрагмент кода, который показывает BLOCKED
состояние?
Что ж, давайте посмотрим, как мы можем написать фрагменты кодов для каждого состояния.
Вам также может понравиться: Как читать дамп темы
Состояния потока Java выражаются посредством Thread.State
перечисления. Возможные состояния потока Java показаны на следующей диаграмме:
Существуют следующие состояния жизненного цикла:
NEW
Состояние.RUNNABLE
Состояние.BLOCKED
Состояние.WAITING
Состояние.TIMED_WAITING
Состояние.TERMINATED
Состояние.
Давайте узнаем обо всех различных состояниях в следующих разделах.
НОВОЕ Государство
Поток Java находится в NEW
состоянии, если он создан, но не запущен (конструктор потока создает потоки в NEW
состоянии). Это его состояние, пока start()
метод не будет вызван. Код в комплекте с этой статьей содержит несколько фрагментов коды , которые показывают это состояние с помощью различных методов строительства, в том числе лямбды. Для краткости ниже приводится лишь одна из этих конструкций:
Джава
xxxxxxxxxx
1
public class NewThread {
2
public void newThread() {
3
Thread t = new Thread(() -> {});
4
System.out.println("NewThread: " + t.getState()); // NEW
5
}
6
}
7
8
//usage
9
NewThread nt = new NewThread();
10
nt.newThread();
БЕЗОПАСНОЕ Государство
Переход от NEW
к RUNNABLE
получен, вызывая start()
метод. В этом состоянии поток может быть запущен или готов к работе. Когда он готов к запуску, поток ожидает, пока планировщик потока JVM не выделит необходимые ресурсы и время для его запуска.
Как только процессор станет доступен, поток-планировщик запустит поток. Следующий фрагмент кода должен напечатать RUNNABLE
, так как мы печатаем состояние потока после вызова start()
. Но из-за внутренних механизмов планировщика потоков это не гарантируется (интервьюеры любят такие подсказки):
Джава
xxxxxxxxxx
1
public class RunnableThread {
2
public void runnableThread() {
3
Thread t = new Thread(() -> {});
4
t.start();
5
6
// RUNNABLE
7
System.out.println("RunnableThread : " + t.getState());
8
}
9
}
10
11
// usage
12
RunnableThread rt = new RunnableThread();
13
rt.runnableThread();
ЗАБЛОКИРОВАНО состояние
Когда поток пытается выполнить задачи ввода-вывода или синхронизированные блоки, он может войти в это BLOCKED
состояние. Например, если поток t1
пытается войти в синхронизированный блок кода, к которому уже обращается другой поток t2
, он t1
остается в BLOCKED
состоянии до тех пор, пока не сможет получить блокировку.
Этот сценарий сформирован в следующем фрагменте кода:
1. Создайте две темы: t1
и t2
.
2. Начните с t1
помощью start()
метода:
1. t1
выполнит run()
метод и получит блокировку для синхронизированного метода syncMethod()
.
2.syncMethod()
будет держать t1
внутри навсегда, так как он имеет бесконечный цикл.
3. Через две секунды (произвольное время) начните с t2
помощью start()
метода:
1. t2
выполнит run()
код и окажется в BLOCKED
состоянии, так как не может получить
замок syncMethod()
.
Фрагмент кода выглядит следующим образом:
Джава
xxxxxxxxxx
1
public class BlockedThread {
2
3
public void blockedThread() {
4
5
Thread t1 = new Thread(new SyncCode());
6
Thread t2 = new Thread(new SyncCode());
7
8
t1.start();
9
Thread.sleep(2000);
10
11
t2.start();
12
Thread.sleep(2000);
13
14
System.out.println("BlockedThread t1: "
15
+ t1.getState() + "(" + t1.getName() + ")");
16
System.out.println("BlockedThread t2: "
17
+ t2.getState() + "(" + t2.getName() + ")");
18
19
System.exit(0);
20
}
21
22
private static class SyncCode implements Runnable {
23
24
25
public void run() {
26
System.out.println("Thread " + Thread.currentThread().getName()
27
+ " is in run() method");
28
syncMethod();
29
}
30
31
public static synchronized void syncMethod() {
32
33
System.out.println("Thread " + Thread.currentThread().getName()
34
+ " is in syncMethod() method");
35
36
while (true) {
37
// t1 will stay here forever, therefore t2 is blocked
38
}
39
}
40
}
41
}
42
43
// usage
44
BlockedThread bt = new BlockedThread();
45
bt.blockedThread();
Вот возможный вывод (названия потоков могут отличаться от приведенных здесь):
Джава
xxxxxxxxxx
1
Thread Thread-0 is in run() method
2
Thread Thread-0 is in syncMethod() method
3
Thread Thread-1 is in run() method
4
BlockedThread t1: RUNNABLE(Thread-0)
5
BlockedThread t2: BLOCKED(Thread-1)
Состояние ожидания
Поток, t1
ожидающий (без периода ожидания) другого потока t2
для завершения, находится в WAITING
состоянии. Этот сценарий сформирован в следующем фрагменте кода:
1. Создать тему: t1
.
2. Начните с t1
помощью start()
метода.
3. В run()
методе t1
:
1. Создайте другую тему: t2
.
2. Начните с t2
помощью start()
метода.
3. Пока t2
работает, звоните t2.join()
— так как t2
необходимо присоединиться t1
(или, другими словами,t1
нужно ждать, t2
чтобы умереть), t1
находится в WAITING
состоянии.
4. В run()
способе t2
, t2
печатает состояние t1
, которое должно быть WAITING
(при печати на t1
состояние, t2
работает, поэтому t1
ждет).
Фрагмент кода выглядит следующим образом:
Джава
xxxxxxxxxx
1
public class WaitingThread {
2
3
public void waitingThread() {
4
new Thread(() -> {
5
Thread t1 = Thread.currentThread();
6
Thread t2 = new Thread(() -> {
7
8
Thread.sleep(2000);
9
System.out.println("WaitingThread t1: "
10
+ t1.getState()); // WAITING
11
});
12
13
t2.start();
14
15
t2.join();
16
17
}).start();
18
}
19
}
20
21
// usage
22
WaitingThread wt = new WaitingThread();
23
wt.waitingThread();
Состояние TIMED_WAITING
Поток, t1
ожидающий определенного периода времени для другого потока t2
, для завершения находится в TIMED_WAITING
состоянии. Этот сценарий сформирован в следующем фрагменте кода:
1. Создать тему: t1
.
2. Начните с t1
помощью start()
метода.
3. В run()
методе t1
добавьте время ожидания в две секунды (произвольное время).
4. Во время t1
работы основной поток печатает t1
состояние — состояние должно быть, TIMED_WAITING
поскольку оно t1
находится в a sleep()
, срок действия которого истечет через две секунды.
Фрагмент кода выглядит следующим образом:
Джава
xxxxxxxxxx
1
public class TimedWaitingThread {
2
3
public void timedWaitingThread() {
4
Thread t = new Thread(() -> {
5
Thread.sleep(2000);
6
});
7
8
t.start();
9
10
Thread.sleep(500);
11
12
System.out.println("TimedWaitingThread t: "
13
+ t.getState()); // TIMED_WAITING
14
}
15
}
16
17
// usage
18
TimedWaitingThread twt = new TimedWaitingThread();
19
twt.timedWaitingThread();
ПРЕКРАЩЕННОЕ Государство
Поток, который успешно завершает свою работу или ненормально прерван, находится в TERMINATE
состоянии. Это очень просто смоделировать, как в следующем фрагменте кода (основной поток приложения печатает состояние потока, t
— когда это происходит, поток t
выполняет свою работу):
Джава
xxxxxxxxxx
1
public class TerminatedThread {
2
3
public void terminatedThread() {
4
Thread t = new Thread(() -> {});
5
t.start();
6
7
Thread.sleep(1000);
8
9
System.out.println("TerminatedThread t: "
10
+ t.getState()); // TERMINATED
11
}
12
}
13
14
// usage
15
TerminatedThread tt = new TerminatedThread();
16
tt.terminatedThread();
Готово! Обычно последний вопрос интервьюера о теме параллелизма Java будет звучать так: Для написания поточно-ориентированных классов какие методы вы рассматриваете? Ну, возможный ответ упомянет следующие методы:
- Не иметь состояния (классы без экземпляра и
static
переменных). - У государства , но не разделяю его (например, использование переменных экземпляра через
Runnable
,ThreadLocal
и так далее). - Есть состояние , но неизменное состояние.
- Используйте передачу сообщений (например, как фреймворк Akka).
- Используйте
synchronized
блоки. - Используйте
volatile
переменные. - Используйте структуры данных из
java.util.concurrent
пакета. - Используйте синхронизаторы (например,
CountDownLatch
иBarrier
). - Используйте замки из
java.util.concurrent.locks
пакета.
Полный код доступен на GitHub .
Если вам понравилась эта статья, вам понравится моя книга « Проблемы кодирования Java» , которая содержит отдельную главу, включающую 27 задач, посвященных теме параллелизма Java.
Дальнейшее чтение
Эволюция проблемы «производитель-потребитель» в Java