Можно ли использовать Runnable для создания нового потока?
Что проверяют
Базу многопоточности: разницу между задачей и потоком, а также между start() и run(). Следом почти всегда спрашивают, чем Runnable отличается от Callable и почему в продакшене потоки руками обычно не создают.
Как отвечать
Да, это основной способ. Runnable описывает задачу, Thread её исполняет. Задача передаётся в конструктор, поток запускается вызовом start():
new Thread(() -> process(order), "order-worker").start();
Runnable помечен @FunctionalInterface, поэтому вместо отдельного класса хватает лямбды. Сам Thread тоже реализует Runnable, но наследоваться от него ради одного run() смысла нет: вы тратите единственное наследование и смешиваете задачу с механизмом запуска.
Дальше покажите, что знаете окружение:
Callable<V>: возвращает результат и может бросать проверяемые исключения,Runnableне умеет ни того, ни другого;ExecutorService: пул переиспользует потоки,submit(task)возвращаетFuture<?>, через который видно завершение и ошибку;- виртуальные потоки (Java 21+):
Thread.ofVirtual().start(task)илиExecutors.newVirtualThreadPerTaskExecutor()для задач, которые в основном ждут ввода-вывода; CompletableFuture.runAsync(task): по умолчанию выполняется в общемForkJoinPool, для блокирующих задач лучше передать свой executor.
Один и тот же объект Runnable можно отдать нескольким потокам, но тогда его поля становятся общим состоянием, и доступ к ним нужно синхронизировать.
Где спотыкаются
Вызывают run() вместо start(). Код выполнится, но в текущем потоке, никакого параллелизма не будет, и на простом примере это не всегда заметно.
Пытаются запустить один Thread дважды. Повторный start() бросает IllegalThreadStateException: поток одноразовый, а задачу можно отдать новому потоку.
Теряют исключения. В голом потоке необработанное исключение уходит в UncaughtExceptionHandler и печатается в stderr, а при executor.submit() оно оседает внутри Future и пропадает молча, если никто не вызвал get().