← Назад к вопросам

Как распараллеливается работа на процессоре

1.8 Middle🔥 201 комментариев
#REST API и микросервисы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Параллелизм на уровне процессора в Java

Это сложная тема, и я разберу её от аппаратного уровня до Java кода.

Аппаратный уровень

Многоядерный процессор: Современные процессоры имеют несколько физических ядер. Каждое ядро может выполнять инструкции независимо. Например, 8-ядерный процессор может выполнять 8 потоков в одно время:

  • Ядро 1 выполняет Thread 1
  • Ядро 2 выполняет Thread 2
  • ...
  • Ядро 8 выполняет Thread 8

Гиперпоточность (Hyperthreading): Intel процессоры позволяют каждому ядру выполнять 2 логических потока (thread). Это 16 потоков на 8-ядерном процессоре.

Планировщик ОС: Если потоков больше, чем ядер, операционная система переключает контекст между потоками (context switching):

Время: 0   1   2   3   4   5
Ядро1: T1  T2  T3  T1  T2  T3
Ядро2: T4  T5  T4  T5  T4  T5

Каждый context switch стоит дорого (очистка кэша, сохранение состояния).

Java потоки

// Создание потока
Thread thread = new Thread(() -> {
    System.out.println("Выполняется в отдельном потоке");
});
thread.start();

// Узнаём количество доступных процессорных ядер
int coreCount = Runtime.getRuntime().availableProcessors();
System.out.println("Ядер: " + coreCount);

Пул потоков (ExecutorService)

Создавать потоки по одному неэффективно. Используй пул:

// Пул с размером = количество ядер
ExecutorService executor = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors()
);

// Отправляем задачи в пул
for (int i = 0; i < 100; i++) {
    executor.submit(() -> {
        doLongRunningTask();
    });
}

// Ждём завершения
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);

ForkJoinPool для разделяй и властвуй

Лучше всего для вычислительно-интенсивных задач:

ForkJoinPool pool = ForkJoinPool.commonPool();

RecursiveTask<Long> task = new RecursiveTask<Long>() {
    protected Long compute() {
        if (array.length <= 1000) {
            // База рекурсии: обработаем сами
            return processDirectly(array);
        }
        // Разделяем массив пополам
        int mid = array.length / 2;
        RecursiveTask<Long> left = new SubTask(array, 0, mid);
        RecursiveTask<Long> right = new SubTask(array, mid, array.length);
        
        left.fork();  // Отправляем в пул
        long rightResult = right.compute();  // Выполняем
        long leftResult = left.join();  // Ждём результат
        
        return leftResult + rightResult;
    }
};

Long result = pool.invoke(task);

Stream с параллелизмом

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);

// Последовательно
int sum1 = numbers.stream()
    .map(n -> expensiveCalculation(n))
    .reduce(0, Integer::sum);

// Параллельно
int sum2 = numbers.parallelStream()
    .map(n -> expensiveCalculation(n))
    .reduce(0, Integer::sum);

Параллельный stream использует ForkJoinPool под капотом.

Синхронизация

Когда несколько потоков обращаются к одним данным, нужна синхронизация:

public class Counter {
    private int count = 0;
    
    // Защищаем от race condition
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

// Или использовать AtomicInteger (лучше)
public class BetterCounter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();
    }
    
    public int getCount() {
        return count.get();
    }
}

Проблемы параллелизма

Race condition:

// Плохо: two threads могут прочитать одно значение
private int balance = 100;
public void withdraw(int amount) {
    if (balance >= amount) {  // Thread A видит 100
        balance -= amount;     // Thread B видит 100, но выполнится позже
    }
}

Deadlock:

// Thread 1 ждёт lock B, Thread 2 ждёт lock A
// Оба повиснут
synchronized(lockA) {
    synchronized(lockB) { }  // Thread 2 ждёт здесь
}

False sharing: Если два потока обновляют соседние переменные в памяти, они конфликтуют в кэше процессора.

Практические рекомендации

  1. Не переусложняй: параллелизм добавляет сложность
  2. Профилируй: убедись, что параллель даёт прирост
  3. Используй готовые инструменты: ExecutorService, ForkJoinPool, Streams
  4. Избегай synchronized: используй concurrent коллекции
  5. Думай об ограничениях: если задача I/O-bound, параллель не поможет

Как это работает в реальном приложении

Tomcat сервер использует пул потоков (по умолчанию 200):

HTTP запрос 1 → Потоk 1
HTTP запрос 2 → Поток 2
...
HTTP запрос N → Поток N

В каждый момент времени максимум N запросов обрабатываются параллельно на N ядрах.

Если больше 200 запросов — они встают в очередь.

Правило большого пальца

  • CPU-bound задачи: threads = количество ядер
  • I/O-bound задачи: threads = ядра * (1 + время_ожидания / время_работы)
  • Обычно: 2 * количество ядер
Как распараллеливается работа на процессоре | PrepBro