Практически любое техническое интервью по 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