Runnable.run: примеры (JAVA)

Метод run интерфейса Runnable - пояснение и сценарии использования
Раздел: Функциональные интерфейсы (использование и создание)
Runnable.run: void

Описание метода run

Метод run принадлежит интерфейсу java.lang.Runnable и имеет сигнатуру public void run(). Аргументов у метода нет, возвращаемое значение - void. Метод не объявляет проверяемых исключений (checked exceptions), поэтому в реализации либо обрабатываются все проверяемые исключения, либо они не используются.

Типичное назначение метода - размещение тела задачи, которое может выполняться в потоке. Варианты вызова:

  • Вызов напрямую: выполнение происходит в текущем потоке.
  • Передача экземпляра в Thread и вызов start(): выполнение происходит в отдельном потоке.
  • Передача в API из пакета java.util.concurrent (например, ExecutorService): задача выполняется одним из потоков пула.

Особенности поведения:

  • При выбросе непроверяемого исключения (RuntimeException или Error) оно приводит к завершению выполняющего потока; в пуле потоков исключение обычно логируется и работа пула продолжается.
  • Интерфейс Runnable является функциональным интерфейсом, поэтому реализацию можно задать через лямбду или ссылку на метод (начиная с Java 8).
  • Вызов run() напрямую не создает новый поток.

Аргументы: отсутствуют. Возвращаемое значение: отсутствует (void). Исключения: метод не декларирует проверяемых исключений; unchecked-исключения распространяются в вызывающий поток.

Короткие примеры использования

Примеры показывают разные способы запуска логики, представленной через Runnable, и визуальную разницу между вызовом run() и запуском через Thread.start() либо ExecutorService.

1) Прямой вызов run() и запуск через Thread:

public class RunExample1 {
  public static void main(String[] args) {
    Runnable r = () -> System.out.println("Выполнение в потоке: " + Thread.currentThread().getName());
    r.run(); // выполняется в текущем (main)
    new Thread(r, "worker-1").start(); // выполняется в новом потоке
  }
}
Выполнение в потоке: main
Выполнение в потоке: worker-1

2) Передача в ExecutorService и получение Future (результат null для Runnable):

import java.util.concurrent.*;

public class RunExample2 {
  public static void main(String[] args) throws Exception {
    ExecutorService ex = Executors.newSingleThreadExecutor();
    Runnable r = () -> System.out.println("Task in " + Thread.currentThread().getName());
    Future<?> f = ex.submit(r);
    System.out.println("Future get: " + f.get());
    ex.shutdown();
  }
}
Task in pool-1-thread-1
Future get: null

3) Лямбда и обработка unchecked-исключения внутри run:

public class RunExample3 {
  public static void main(String[] args) {
    Runnable r = () -> {
      System.out.println("Начало");
      throw new RuntimeException("Ошибка в задаче");
    };
    try {
      r.run();
    } catch (RuntimeException ex) {
      System.out.println("Поймано: " + ex.getMessage());
    }
  }
}
Начало
Поймано: Ошибка в задаче

Похожие API в Java и их особенности

  • Callable<V> - похожая роль, но возвращает значение или выбрасывает проверяемое исключение. Предпочтительна при необходимости результата.
  • FutureTask<V> / RunnableFuture - комбинирует Runnable и Future, позволяет запускать и получить результат/исключение.
  • Thread (унаследовать класс) - дает возможность расширить поведение потока, но композиция с Runnable обычно предпочтительнее.
  • ForkJoinTask - специализирован для fork/join-парадигмы и рекурсивного распараллеливания; предпочтительнее при задачах, разбиваемых на подзадачи.
  • CompletableFuture - удобен для асинхронных цепочек и композиции задач с результатами и обработкой ошибок.

Аналоги в других языках и отличия

Краткие примеры показывают, как в других языках задаётся логика, эквивалентная Java Runnable.run.

Python (threading) - целевая функция передаётся в Thread:

import threading

def task():
    print('Выполняется в', threading.current_thread().name)

threading.Thread(target=task, name='py-worker').start()
print('main поток')
main поток
Выполняется в Thread-1

C# (ThreadStart / Task) - делегат ThreadStart похож на Runnable; Task даёт удобства:

using System;
using System.Threading;

class Example {
  static void Main() {
    Thread t = new Thread(() => Console.WriteLine("Выполнение в " + Thread.CurrentThread.ManagedThreadId));
    t.Start();
    Console.WriteLine("main");
  }
}
main
Выполнение в 3

JavaScript - в однопоточном окружении нет прямого эквивалента; в браузере используется Web Worker для параллелизма, в Node.js - worker_threads. Часто применяется асинхронность через колбэки и промисы.

Go - горутины: вызов функции с go выполняет её параллельно, возвращаемого значения нет; модель отличается управлением планировщиком языка.

package main
import (
  "fmt"
  "time"
)
func main() {
  go func() { fmt.Println("в горутине") }()
  time.Sleep(10 * time.Millisecond)
  fmt.Println("main")
}
в горутине
main

Kotlin - использует те же Runnable/Thread; дополнительно есть корутины (kotlinx.coroutines) с более богатым API для асинхронности и отмены.

Lua - корутины кооперативные, работают иначе, не дают параллелизма на уровне ядра.

PHP - стандартно без потоков; расширение pthreads предоставляет похожую модель, но применяется редко в веб-среде.

Основное отличие от Java: формы запуска и модель памяти. В Java Runnable интегрирован с моделью потоков JVM и API java.util.concurrent; в других языках используются или системные потоки, или встроенные легковесные примитивы (горутины, корутины).

Типичные ошибки и примеры

Список распространённых ошибок и демонстрации их проявления.

1) Вызов run() вместо start() при попытке создать новый поток:

public class ErrorExample1 {
  public static void main(String[] args) {
    Thread t = new Thread(() -> System.out.println("Поток: " + Thread.currentThread().getName()));
    t.run(); // НЕ запускает новый поток
    System.out.println("После вызова run");
  }
}
Поток: main
После вызова run

Ожидание: вывод из отдельного потока. Фактически задача выполнилась в main.

2) Ожидание результата от Runnable при submit: результат null:

// аналогично примеру в разделе examples
// После submit(runnable).get() возвращается null, что иногда удивляет.
Future get: null

3) Неправильная обработка прерываний внутри run (игнорирование Thread.interrupt):

public class ErrorExample3 {
  public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
      while (true) {
        // не проверяется interrupted, бесконечный цикл
      }
    });
    t.start();
    t.interrupt();
    System.out.println("Попытка прерывания отправлена");
  }
}
Попытка прерывания отправлена
// поток может не завершиться, так как цикл не реагирует на прерывание

4) Гонка данных при доступе к общему состоянию без синхронизации:

public class ErrorExample4 {
  static int counter = 0;
  public static void main(String[] args) throws InterruptedException {
    Runnable r = () -> {
      for (int i = 0; i < 10000; i++) counter++;
    };
    Thread t1 = new Thread(r), t2 = new Thread(r);
    t1.start(); t2.start();
    t1.join(); t2.join();
    System.out.println(counter); // часто < 20000
  }
}
Значение может быть отличным от 20000 из-за условий гонки

Изменения и эволюция

Сигнатура метода run() не менялась с ранних версий Java: это простой метод без аргументов и возвращаемого значения.

  • Java 8: Runnable помечен как функциональный интерфейс, что позволило использовать лямбды и ссылки на методы.
  • Современные версии JVM и библиотеки: Project Loom (Java 19+ в виде предварительных сборок и далее) вводит виртуальные потоки, которые изменяют модель запуска потоков, но метод run() и интерфейс Runnable остаются прежними. Разница - в характеристиках создаваемого потока (легковесные виртуальные потоки), а не в API Runnable.
  • Других изменений в самом интерфейсе не происходило; дополнительные утилиты и адаптеры появились в java.util.concurrent.

Расширенные и нетипичные примеры

Несколько продвинутых приёмов с кодом и выводом.

1) Обёртка Runnable для централизованной обработки исключений и логирования:

Пример java
import java.util.concurrent.*;

public class AdvExample1 {
  static Runnable safe(Runnable r) {
    return () -> {
      try {
        r.run();
      } catch (Throwable t) {
        System.err.println("Ошибка в задаче: " + t.getMessage());
      }
    };
  }

  public static void main(String[] args) {
    ExecutorService ex = Executors.newSingleThreadExecutor();
    ex.submit(safe(() -> { throw new RuntimeException("boom"); }));
    ex.shutdown();
  }
}
Ошибка в задаче: boom

2) Преобразование Runnable в Callable, чтобы вернуть значение после выполнения:

Пример java
import java.util.concurrent.*;

public class AdvExample2 {
  static  Callable toCallable(Runnable r, T result) {
    return () -> { r.run(); return result; };
  }
  public static void main(String[] args) throws Exception {
    Callable c = toCallable(() -> System.out.println("Выполняется"), "готово");
    System.out.println(c.call());
  }
}
Выполняется
готово

3) Планирование повторяющейся задачи и отмена через Future:

Пример java
import java.util.concurrent.*;

public class AdvExample3 {
  public static void main(String[] args) throws Exception {
    ScheduledExecutorService sch = Executors.newSingleThreadScheduledExecutor();
    Runnable r = () -> System.out.println("tick " + System.currentTimeMillis());
    ScheduledFuture<?> f = sch.scheduleAtFixedRate(r, 0, 200, TimeUnit.MILLISECONDS);
    Thread.sleep(650);
    f.cancel(false);
    sch.shutdown();
    System.out.println("Cancelled");
  }
}
tick 1610000000000
tick 1610000000200
tick 1610000000400
Cancelled

4) Комбинация Runnables: последовательное и параллельное исполнение в рамках пула:

Пример java
import java.util.concurrent.*;

public class AdvExample4 {
  static Runnable seq(Runnable a, Runnable b) {
    return () -> { a.run(); b.run(); };
  }
  public static void main(String[] args) throws InterruptedException {
    ExecutorService ex = Executors.newFixedThreadPool(2);
    Runnable a = () -> System.out.println("A " + Thread.currentThread().getName());
    Runnable b = () -> System.out.println("B " + Thread.currentThread().getName());
    ex.submit(seq(a,b));
    ex.shutdown();
    ex.awaitTermination(1, TimeUnit.SECONDS);
  }
}
A pool-1-thread-1
B pool-1-thread-1

5) Запуск на виртуальном потоке (Java с поддержкой Loom):

Пример java
// Требуется Java с виртуальными потоками (Project Loom)
public class AdvExample5 {
  public static void main(String[] args) {
    Runnable r = () -> System.out.println("Виртуальный: " + Thread.currentThread());
    Thread t = Thread.ofVirtual().unstarted(r);
    t.start();
  }
}
Виртуальный: Thread[VirtualThread-1,5,main]

6) Использование ThreadLocal внутри Runnable для сохранения контекста:

Пример java
public class AdvExample6 {
  static ThreadLocal ctx = ThreadLocal.withInitial(() -> "undef");
  public static void main(String[] args) throws InterruptedException {
    Runnable r = () -> {
      ctx.set("ctx-" + Thread.currentThread().getName());
      System.out.println(ctx.get());
    };
    Thread t = new Thread(r, "t1");
    t.start(); t.join();
  }
}
ctx-t1

Каждый пример демонстрирует приёмы для повышения надёжности, гибкости и интеграции Runnable в современные конструкции Java.

джава Runnable.run function comments

En
Runnable.run When an object implementing interface Runnable is used to create a thread, starting the thread causes the object's run method to be called