В чем разница между Runnable и Callable в Java и когда использовать каждый из них
Что проверяют
Знаете ли вы, чем задача с результатом отличается от задачи «запустил и забыл», и понимаете ли, куда деваются исключения в пуле потоков. Следом почти всегда спрашивают про разницу execute() и submit() или просят показать, как получить результат из Future.
Как отвечать
Начните с сигнатур, из них следует всё остальное:
Runnable: интерфейс изjava.lang, есть с первой версии Java. Методvoid run()ничего не возвращает и не объявляет проверяемых исключений, поэтомуIOExceptionвнутри придётся ловить и заворачивать;Callable<V>: появился в Java 5 вjava.util.concurrent. МетодV call() throws Exceptionвозвращает результат и может бросить что угодно;- где принимаются:
ThreadиExecutorService.execute()берут толькоRunnable.submit()перегружен для обоих и возвращаетFuture, дляRunnableегоget()вернётnull.invokeAll()иinvokeAny()работают с коллекциейCallable; - переходники:
Executors.callable(runnable)превращаетRunnableвCallable, аFutureTaskпозволяет запуститьCallableв обычномThread.
Отдельно скажите про исключения. Упавший Callable не роняет поток: ошибка сохраняется в Future и выбрасывается из get() обёрнутой в ExecutionException, настоящую причину достают через getCause().
Правило выбора простое: нужен результат или проверяемое исключение, берите Callable, иначе хватит Runnable. У CompletableFuture свои интерфейсы: runAsync() принимает Runnable, а supplyAsync() принимает Supplier, не Callable.
Где спотыкаются
Говорят, что Runnable вообще не может бросить исключение. Может, но только непроверяемое: при execute() оно уйдёт в обработчик неперехваченных исключений потока, а при submit() осядет в Future.
Отправляют задачу через submit() и никогда не вызывают get(). Исключение тихо лежит внутри Future, в логах пусто, а задача просто «не сработала».
Вызывают future.get() без таймаута и глотают InterruptedException. Поток может зависнуть навсегда, а флаг прерывания теряется, хотя его стоит восстановить через Thread.currentThread().interrupt().