Как работать с потоками (Thread) в Java?

«Как работать с потоками (Thread) в Java?» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Работа с потоками в Java осуществляется через класс Thread и интерфейс Runnable (или Callable). Современные подходы рекомендуют использование высокоуровневых API, таких как ExecutorService.

1. Создание и запуск потока

Способ 1: Наследование от Thread (не рекомендуется из-за ограничения наследования)

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running: " + Thread.currentThread().getName());
    }
}
// Запуск
new MyThread().start();

Способ 2: Реализация Runnable (предпочтительнее)

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable is running: " + Thread.currentThread().getName());
    }
}
// Запуск
Thread thread = new Thread(new MyRunnable());
thread.start();

2. Использование Executor Framework (Java 5+)

Этот фреймворк управляет пулом потоков, избавляя от ручного создания Thread.

import java.util.concurrent.*;

// Создание пула с фиксированным количеством потоков
ExecutorService executor = Executors.newFixedThreadPool(4);

// Задача без возвращаемого значения (Runnable)
executor.execute(() -> {
    System.out.println("Async task executed by: " + Thread.currentThread().getName());
});

// Задача с возвращаемым значением (Callable)
Future<Integer> future = executor.submit(() -> {
    Thread.sleep(1000);
    return 42;
});

// Получение результата (блокирующий вызов)
try {
    Integer result = future.get(); // Ждёт завершения задачи
    System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

// Корректное завершение пула
executor.shutdown();
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow();
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
}

3. Критические секции и синхронизация

Для предотвращения состояния гонки (race condition) используется синхронизация.

Синхронизированный метод:

public class Counter {
    private int count = 0;
    public synchronized void increment() { count++; } // Монитор — this
    public synchronized int getCount() { return count; }
}

Синхронизированный блок:

public void addItem(List<String> list, String item) {
    synchronized(list) { // Монитор — объект list
        list.add(item);
    }
}

4. Современные подходы (Java 8+)

  • CompletableFuture: Для асинхронного программирования и комбинирования задач.
    CompletableFuture.supplyAsync(() -> fetchDataFromAPI())
                    .thenApply(data -> processData(data))
                    .thenAccept(result -> System.out.println(result))
                    .exceptionally(ex -> { ex.printStackTrace(); return null; });
  • Параллельные стримы (Parallel Streams): Для параллельной обработки коллекций.
    List<Integer> numbers = List.of(1, 2, 3, 4, 5);
    int sum = numbers.parallelStream()
                     .mapToInt(Integer::intValue)
                     .sum();

Ключевые принципы:

  • Не создавайте потоки вручную (new Thread()) для каждой задачи, используйте ExecutorService.
  • Всегда обрабатывайте InterruptedException корректно.
  • Для совместного доступа к изменяемым данным используйте потокобезопасные коллекции из java.util.concurrent (например, ConcurrentHashMap, CopyOnWriteArrayList).