Потоки выполнения
Поток выполнения (thread) — это отдельная последовательность команд внутри программы. Обычно программа выполняет код строка за строкой в одном потоке — главном, он называется main. Если запустить несколько потоков, они работают одновременно: пока один ждёт ответа от сети, другой считает данные. Не путайте их с потоками данных (Stream API) — это разные понятия.
Первый поток
Поток создают классом Thread, передав ему задачу — лямбду без параметров. Метод start() запускает поток, а join() заставляет текущий поток дождаться его завершения:
void main() throws InterruptedException {
Thread worker = new Thread(() -> IO.println("Привет из другого потока"));
IO.println("Запускаем");
worker.start();
worker.join();
IO.println("Готово");
}Результат:
Запускаем
Привет из другого потока
ГотовоБлагодаря join() строка «Готово» гарантированно выводится последней. join и другие методы ожидания могут выбросить проверяемое InterruptedException — если поток попросят прервать ожидание, — поэтому у main стоит throws.
Порядок не гарантирован
Потоки работают независимо, и операционная система сама решает, какому из них дать процессор. Поэтому порядок вывода двух потоков меняется от запуска к запуску:
void count(String name) {
for (int i = 1; i <= 3; i++) {
IO.println(name + i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
return;
}
}
}
void main() throws InterruptedException {
Thread a = new Thread(() -> count("A"));
Thread b = new Thread(() -> count("B"));
a.start();
b.start();
a.join();
b.join();
}Результат (один из возможных — у вас порядок может быть другим):
A1
B1
B2
A2
B3
A3Внутри одного потока порядок сохраняется: A1 всегда раньше A2. А вот как строки A и B перемешаются, предсказать нельзя.
Паузы
Thread.sleep приостанавливает текущий поток на заданное время. Это удобно, чтобы имитировать долгую работу — запрос к серверу или чтение большого файла:
void main() throws InterruptedException {
IO.println("Загружаю...");
Thread.sleep(Duration.ofMillis(500));
IO.println("Загружено");
}Результат:
Загружаю...
ЗагруженоИмена потоков
У каждого потока есть имя. Его возвращает Thread.currentThread().getName() — так можно узнать, в каком потоке выполняется код. Задать имя удобно через построитель Thread.ofPlatform():
void main() throws InterruptedException {
Thread worker = Thread.ofPlatform()
.name("загрузчик")
.start(() -> IO.println("Работает: " + Thread.currentThread().getName()));
worker.join();
IO.println("Работает: " + Thread.currentThread().getName());
}Результат:
Работает: загрузчик
Работает: mainМетод start построителя сразу и создаёт, и запускает поток.
Результат из потока
Лямбда в Thread ничего не возвращает. Простой способ собрать результаты — дать каждому потоку свою ячейку массива и прочитать массив после join:
void main() throws InterruptedException {
int[] sums = new int[2];
Thread first = new Thread(() -> sums[0] = IntStream.rangeClosed(1, 500).sum());
Thread second = new Thread(() -> sums[1] = IntStream.rangeClosed(501, 1000).sum());
first.start();
second.start();
first.join();
second.join();
IO.println("Сумма от 1 до 1000: " + (sums[0] + sums[1]));
}Результат:
Сумма от 1 до 1000: 500500Потоки пишут в разные ячейки и не мешают друг другу, а join гарантирует, что к моменту чтения оба закончили работу. Удобнее получать результаты через пулы потоков — о них следующий урок.
Частая ошибка
Если вызвать run() вместо start(), новый поток не создаётся: задача просто выполнится в текущем потоке, как обычный метод:
void main() {
Thread worker = new Thread(() -> IO.println("Выполняюсь в " + Thread.currentThread().getName()));
worker.run();
}Результат:
Выполняюсь в mainОшибки нет, но и параллельности тоже. Чтобы код выполнился в отдельном потоке, всегда вызывайте start().