Многопоточность в Java

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

Concurrency и parallelism

  • Concurrency — несколько задач продвигаются во времени, возможно на одном ядре.
  • Parallelism — несколько задач физически выполняются одновременно на разных ядрах.

Потоки полезны для двух разных целей:

  1. CPU-bound задачу можно разбить на независимые вычисления и использовать несколько ядер.
  2. В I/O-bound задаче один поток может выполнять работу, пока другой ждет сеть или диск.

Больше потоков не означает быстрее: появляются переключения контекста, конкуренция за блокировки, память под стеки и нагрузка на планировщик.

Процесс и поток

СвойствоПроцессПоток
Адресное пространствосвоеобщее с потоками процесса
Изоляция сбоявышенеобработанная ошибка портит общий процесс, хотя не обязательно завершает все потоки
Обмен даннымиIPC, сокеты, файлыобщие объекты и синхронизация
Создание и переключениеобычно дорожеобычно дешевле

Три задачи многопоточного кода

  1. Исполнение: Thread, ExecutorService, ForkJoinPool.
  2. Безопасная публикация и видимость: synchronized, volatile, финальные поля, concurrent-коллекции; см. Java Memory Model и Happens before.
  3. Координация: Future, очереди, latch/barrier/phaser, wait/notify.
ExecutorService pool = Executors.newFixedThreadPool(4);
try {
    Future<Integer> result = pool.submit(() -> compute());
    System.out.println(result.get());
} finally {
    pool.shutdown();
}

Для прикладного кода задача обычно отделяется от механизма запуска: Runnable или Callable передают исполнителю, а не создают новый Thread на каждый вызов.

Карта заметок

На собеседовании

  • Могут ли потоки выполняться конкурентно на одном ядре? Да, планировщик чередует их.
  • Что разделяют потоки Java-процесса? Кучу и ресурсы процесса; стек у каждого свой.
  • Почему counter++ небезопасен? Это чтение, вычисление и запись, между которыми может вклиниться другой поток.
  • Что важнее числа потоков? Характер нагрузки, число ядер, доля ожидания, ограничения внешних ресурсов и цена координации.
  • Дает ли Thread.sleep() синхронизацию памяти? Нет.