Как запустить поток
Пока на нашей кухне работает один повар — главный поток main. Чтобы нанять второго, нужны две вещи: задача, которую он будет делать, и сам повар, который за неё возьмётся.
Задача и поток
Задачу в Java описывает интерфейс Runnable из стандартной библиотеки. В нём один метод, void run(), и в его теле записано, что нужно сделать. Метод один, поэтому Runnable удобно записать лямбдой: () -> System.out.println("cook").
Повара описывает класс Thread. Его конструктор принимает задачу: new Thread(task). Сам объект Thread ещё ничего не делает — это повар, которого наняли, но пока не поставили к плите.
Вот программа, в которой работают два потока:
public class Main {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> System.out.println("cook");
Thread t = new Thread(task);
t.start();
System.out.println("main");
t.join();
System.out.println("done");
}
}Давайте разберём её по шагам:
Runnable task = …— задача. Она только записана, и ничего пока не выполняется.new Thread(task)создаёт объект потока, но поток ещё не работает.t.start()запускает новый поток, и тот выполняетrun()нашей задачи. Самstart()сразу возвращает управление и ничего не ждёт.mainидёт дальше, не дожидаясьt, и печатаетmain.t.join()ждёт, пока потокtзакончит.- Программа печатает
done.
Строки cook и main могут прийти в любом порядке. Обычно первым успевает main: новому потоку нужно время, чтобы запуститься. Но решает операционная система, а не порядок строк в коде, и иногда первой оказывается строка cook. Строка done всегда последняя: перед ней main дождался t.
join и InterruptedException
Вызов join() значит «подожди, пока этот повар закончит». Без него done могла бы напечататься раньше cook. Строка cook при этом всё равно бы появилась: JVM не завершает программу, пока работает хоть один обычный поток.
Почему у main появилось throws InterruptedException? Пока поток ждёт в join(), его могут попросить прерваться, и тогда join() бросает проверяемое исключение InterruptedException. Проверяемое исключение нужно поймать или объявить, а нашу программу прерывать некому, поэтому достаточно объявить его в заголовке main. Без throws компилятор ответит unreported exception InterruptedException; must be caught or declared to be thrown.
start и run
Этот вопрос часто задают на собеседовании: чем start() отличается от run()? У объекта Thread есть оба метода, но делают они разное:
start()создаёт новый поток, и уже этот поток вызываетrun();run()— обычный вызов метода: задача выполняется в том потоке, который его вызвал, и нового потока не появляется.
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
});
t.run(); // main
t.start(); // Thread-0
t.join();
}
}Разберём, что происходит:
- Задача печатает имя того потока, который её выполняет.
t.run()выполняет задачу прямо вmain, как вызов любого другого метода, и печатаетсяmain.t.start()запускает новый поток, и задача печатает его имя —Thread-0. Потоки без имени JVM нумерует сама:Thread-0,Thread-1и так далее.
Имя можно дать и самим, вторым аргументом конструктора: поток new Thread(task, "cook") называется cook.
Вызвать run() вместо start() — всё равно что главный повар сам приготовил блюдо по рецепту помощника. Блюдо готово, но помощник так и не встал к плите.
Виртуальные потоки
Обычный поток Java — это поток операционной системы, и он недешёв: под него сразу резервируют память, обычно около мегабайта. Тысячи таких потоков программа выдержит, миллион — уже нет.
В Java 21 появились виртуальные потоки — лёгкие потоки, которыми управляет сама JVM. Она раскладывает много виртуальных потоков на немного обычных. Виртуальных потоков можно запустить миллион, и они особенно полезны там, где задача в основном ждёт: ответа по сети или от базы данных. Здесь картинка с кухней перестаёт работать: миллион поваров на одну кухню не нанять, а миллион виртуальных потоков в одной программе запустить можно.
Thread v = Thread.startVirtualThread(() -> {
System.out.println("virtual cook"); // virtual cook
});
v.join();
System.out.println(v.isVirtual()); // trueРазберём по строкам:
Thread.startVirtualThread(…)создаёт виртуальный поток и сразу его запускает: вызыватьstart()не нужно.v.join()ждёт его, как и обычный поток.v.isVirtual()подтверждает, что поток виртуальный, и программа печатаетtrue.
Здесь join() обязателен. JVM не ждёт виртуальные потоки, и без join() программа может закончиться раньше, чем задача успеет что-то напечатать.
Частая ошибка
Один и тот же поток запустили второй раз:
Thread t = new Thread(() -> System.out.println("cook"));
t.start();
t.join();
t.start(); // ✗Exception in thread "main" java.lang.IllegalThreadStateExceptionПрограмма компилируется, печатает cook и останавливается на втором start(). Поток запускается только один раз, и закончившийся поток снова не запустить. Чтобы выполнить задачу ещё раз, создайте новый поток: new Thread(task).start().
Главное
- Задачу описывает
Runnable, поток — объектThread, а запускает новый потокstart(). run()— обычный вызов метода: задача выполняется в текущем потоке, и нового потока нет.join()ждёт, пока поток закончит; без него порядок вывода разных потоков не задан.- Виртуальный поток (Java 21) запускает
Thread.startVirtualThread(…); JVM его не ждёт, поэтому нуженjoin().