W3docs

Интерфейс Java Runnable

Определяйте единицы работы для потоков с помощью функционального интерфейса Runnable — предпочтительный способ для потоков, executor и виртуальных потоков.

Runnable — это интерфейс с единственным методом, возможно, самый важный в java.lang. Всё, что «выполняется в потоке» в Java, в конечном счёте является Runnable: конструктор Thread принимает его, ExecutorService.execute принимает его, обработчики завершения работы JVM принимают его. Причина, по которой предыдущая глава рекомендовала «передавать Runnable в конструктор Thread» вместо «расширять Thread», состоит в том, что Runnable разделяет что выполняется и что это выполняет. Именно это разделение позволяет одной и той же задаче работать на платформенном потоке, пуле потоков или виртуальном потоке без изменения кода.

Структура

Всё определение укладывается в три строки:

@FunctionalInterface
public interface Runnable {
  void run();
}

Вот и всё. Из этих трёх строк следует два вывода:

  • Это функциональный интерфейс. Любая лямбда или ссылка на метод с сигнатурой без аргументов и типом возврата void реализует его: () -> System.out.println("hi"), this::flush, Foo::staticMethod.
  • Метод возвращает void и не выбрасывает проверяемых исключений. Это предел того, что можно выразить. Если вам нужен результат или нужно выбросить проверяемое исключение, необходим Callable (через одну-две главы).

Три способа написать Runnable

// 1. Lambda — the modern default
Runnable r1 = () -> System.out.println("hello");

// 2. Method reference — when an existing method has the right signature
Runnable r2 = System.out::flush;

// 3. Anonymous class — pre-Java-8 form, occasionally useful when the body needs fields
Runnable r3 = new Runnable() {
  @Override public void run() {
    System.out.println("hello");
  }
};

Все три варианта создают объект типа Runnable. Лямбда-форма предпочтительна начиная с Java 8; форма анонимного класса полезна только тогда, когда вам нужны собственные поля (что обычно не требуется — вместо этого захватывайте локальные переменные).

Как используется Runnable

Три основных API, принимающих Runnable:

new Thread(runnable).start();                 // platform thread, dedicated
executor.execute(runnable);                   // thread pool or virtual thread
Runtime.getRuntime().addShutdownHook(new Thread(runnable));  // JVM shutdown

Один и тот же экземпляр Runnable работает во всех трёх контекстах. В этом и заключается замысел: что (работа) и где (поток) ортогональны. Вы можете написать код, выполняющий работу, а кто-то другой решит, на чём его запустить.

Контраст с формой подкласса Thread делает это наглядным:

// Coupled: this work can only run on its own dedicated platform thread.
class ImageResizer extends Thread {
  @Override public void run() { resize(); }
}
new ImageResizer().start();

// Decoupled: the same body runs anywhere.
Runnable resize = this::resize;
new Thread(resize).start();                  // dedicated thread
executor.execute(resize);                    // pool
virtualExecutor.execute(resize);             // virtual thread

Именно поэтому в продакшн-коде на Java полно RunnableCallable) и почти никогда нет классов, расширяющих Thread.

Захватываемые переменные должны быть effectively final

Лямбда, становящаяся Runnable, может читать локальные переменные объемлющего метода, но только те, которые компилятор может признать effectively final — присвоенными ровно один раз и никогда не переприсваиваемыми:

String name = "alice";
int n = 3;
Runnable r = () -> {
  for (int i = 0; i < n; i++) {
    System.out.println(name + " " + i);
  }
};
// n = 4;                                   // would break the lambda above — compile error

Если вам нужно разделяемое изменяемое состояние, захваченная локальная переменная не подойдёт — нужно поле, AtomicInteger, элемент массива или другой объект с изменяемым внутренним состоянием. Ограничение намеренное: лямбды захватывают значения, а не псевдонимы, и запрет переприсваивания — простейшее правило, делающее это последовательным.

Наиболее распространённый обходной путь — массив из одного элемента:

int[] counter = {0};
Runnable r = () -> counter[0]++;             // works; the array reference is final, the int inside isn't

Но для потокобезопасных разделяемых счётчиков AtomicInteger — правильный инструмент; через несколько глав мы разберём почему.

Обработка исключений: нечего перехватывать, нечего восстанавливать

run() не выбрасывает проверяемых исключений. Если ваш обработчик может завершиться с проверяемым исключением, его нужно перехватить внутри run():

Runnable parseFile = () -> {
  try {
    Files.readAllLines(path);
  } catch (IOException e) {
    log.error("parse failed", e);            // you HAVE to handle it here
  }
};

С непроверяемыми исключениями ситуация хуже: в вызывающем коде их никто не перехватывает. Если ваш Runnable выбросит NullPointerException в отдельном потоке, исключение попадёт в обработчик непойманных исключений этого потока и поток завершится. Главный поток ничего не узнает.

Два способа справиться с этим:

  1. Перехватывайте всё внутри run() и логируйте самостоятельно. Грубо, но надёжно.
  2. Используйте Callable и Future.get(). Future повторно выбрасывает исключение в потоке, вызвавшем get(). Именно это даёт вам executor framework.

Для разовой работы подходит вариант 1; если задача должна вернуть результат вызывающей стороне, правильный ответ — вариант 2.

Runnable vs. Callable

Сравнение двух интерфейсов задач — с Callable вы познакомитесь позже, но контраст полезен уже сейчас:

RunnableCallable<V>
Методvoid run()V call() throws Exception
Возвращаемое значениеНетТипизированный результат V
Проверяемые исключенияНе может выбрасыватьМожет выбрасывать любое Exception
Принимаетсяnew Thread, Executor.execute, shutdown hooksExecutorService.submit
Дескриптор результатаНет (fire and forget)Future<V>

Если вам нужно либо возвращаемое значение, либо возможность выбрасывать проверяемые исключения — переходите на Callable. Для работы с чистыми побочными эффектами — сброс буфера, логирование, планирование — Runnable является более лёгким инструментом.

Практический пример: один Runnable, три исполнителя

Программа ниже определяет один Runnable, выполняющий небольшую работу, а затем запускает тот же экземпляр (а) в новом платформенном потоке, (б) в ExecutorService и (в) в вызывающем потоке напрямую через .run(). Одно и то же тело выполняется во всех трёх контекстах; меняется только исполнитель.

java— editable, runs on the server

Что можно извлечь из этого запуска:

  • Первые три блока запустили один и тот же экземпляр greet в трёх разных исполнителях — прямой вызов, выделенный поток, пул потоков. Имя потока, которое печатает greet, каждый раз менялось: main, dedicated-worker, pool-1-thread-1. В этом и заключается главная причина предпочитать Runnable подклассу Thread: работа переиспользуется, исполнитель подключаемый.
  • RuntimeException потока crashy не убил main. Он завершился в своём потоке, и обработчик непойманных исключений сообщил о нём. Без обработчика JVM печатает стек-трейс в stderr, а остальная программа продолжает работу — что зачастую хуже, потому что работа, которую должен был выполнить поток, тихо не была сделана.
  • Лямбда shout захватила name и n из локальных переменных main. Они effectively final — присвоены однажды, никогда не переприсваиваются. Добавьте n = 4; в любом месте после определения лямбды — и файл перестанет компилироваться. Это ограничение делает захват в лямбдах безопасным между потоками.
  • В примере bump использовался AtomicInteger, потому что два потока инкрементировали один и тот же счётчик. С обычным полем int итоговое значение оказалось бы где-то между 1000 и 2000 — потерянные обновления из-за неатомарного i++. incrementAndGet() — простейшее исправление, и мы вернёмся к этому в главе об атомарных переменных.
  • Единственный разделяемый экземпляр Runnable был передан в new Thread(bump, "a") и new Thread(bump, "b")одна и та же лямбда выполнялась в двух потоках одновременно. Лямбда не имеет собственных полей; всё, к чему она обращается, живёт вне её. Такова форма каждого безопасного параллельного Runnable: как можно меньше внутреннего состояния, а состояние выносится в потокобезопасный объект, разделяемый потоками.

Что дальше

В следующей главе, Жизненный цикл потока Java, рассматриваются шесть значений Thread.StateNEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED — и показывается, как читать дамп потоков, раскрывающий их.

Практика

Практика
Какое утверждение о `Runnable` верно?
Какое утверждение о `Runnable` верно?
Was this page helpful?