Как работать с потоками (Thread) в 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).

Ответ 18+ 🔞

А, потоки в Java, говоришь? Ну, это как история про Герасима и Муму, только вместо немого мужика у тебя процессор, а вместо собачки — твоя программа, которая пытается не утонуть в конкурентном дерьме. Слушай сюда, я тебе сейчас разложу, как не наступить на эти грабли, блядь.

Вот смотри, раньше, до революции, в Java 1.0, все делали потоки по-простому, по-деревенски: наследуешься от Thread и поехали. Типа так:

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

Выглядит вроде норм, да? А на деле это как прибивать картину кувалдой — вроде работает, но стена вся в пизду. Почему? Да потому что наследование — штука одна, а если твой класс уже от кого-то унаследован? Всё, пиши пропало, в рот меня чих-пых! Поэтому умные дядьки придумали интерфейс 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();

Но и это, скажу я тебе, уже каменный век, ёпта. Потому что если ты начнёшь вручную плодить потоки на каждую чих-пых операцию, твоё приложение сожрёт всю память и процессор, как здоровый немой Герасим сожрал бы твой обед, не поперхнувшись. Поток — он тяжёлый, создавать его — дорого.

И тут, на тебе, в Java 5 подъезжает целый Executor Framework на белом коне! Это, блядь, как управляющий для твоих потоков-работяг. Ты ему задачи кидаешь, а он уже сам решает, кому и когда их выполнять, используя пул готовых работяг. Красота!

import java.util.concurrent.*;

// Создаём пул, скажем, из 4 потоков. Четыре Герасима, которые будут молча и эффективно работать.
ExecutorService executor = Executors.newFixedThreadPool(4);

// Кидаем задачу без ответа (Runnable). Просто сделай что-то, и всё.
executor.execute(() -> {
    System.out.println("Async task executed by: " + Thread.currentThread().getName());
});

// А вот если нужен ответ, то это Callable. Он возвращает Future — это как расписка "должен 42".
Future<Integer> future = executor.submit(() -> {
    Thread.sleep(1000); // Поток поспит, а пул-то не простаивает!
    return 42;
});

// Чтобы получить эти 42, надо по расписке прийти. get() — это блокирующий вызов, он будет ждать, пока Герасим не принесёт ответ.
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(); // Если за 60 секунд не закончили, посылаем всех нахуй.
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
}

А теперь, внимание, самый сок, блядь! Синхронизация. Это когда твои потоки-Герасимы начинают драться за одну и ту же миску с едой (общие данные). Если не контролировать — будет 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);
    }
}

Ну и современная магия, Java 8+: CompletableFuture. Это уже не просто пул, это целый оркестр, где ты можешь строить цепочки: сделай это, потом обработай то, потом выведи результат, а если ошибка — обработай и её. Красота, ядрёна вошь!

CompletableFuture.supplyAsync(() -> fetchDataFromAPI())
                .thenApply(data -> processData(data))
                .thenAccept(result -> System.out.println(result))
                .exceptionally(ex -> { ex.printStackTrace(); return null; });

Или параллельные стримы, если надо быстро прогнать кучу данных:

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sum = numbers.parallelStream()
                 .mapToInt(Integer::intValue)
                 .sum();

Запомни, чувак, три заповеди:

  1. Не плоди потоки вручную. Используй ExecutorService, не будь дикарём.
  2. InterruptedException — не игнорируй его. Это сигнал "эй, тебя хотят прервать, будь добр, отреагируй". Просто проглатывать его — моветон.
  3. Для общих данных — потоко-безопасные инструменты. Бери ConcurrentHashMap, CopyOnWriteArrayList из пакета java.util.concurrent. Не изобретай велосипед с синхронизацией на коленке.

Вот так-то. А то начнёшь делать new Thread() на каждый чих, а потом удивляешься, почему всё виснет и падает. Думай, бошка!