Как распараллеливается работа на процессоре
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Параллелизм на уровне процессора в 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: Если два потока обновляют соседние переменные в памяти, они конфликтуют в кэше процессора.
Практические рекомендации
- Не переусложняй: параллелизм добавляет сложность
- Профилируй: убедись, что параллель даёт прирост
- Используй готовые инструменты: ExecutorService, ForkJoinPool, Streams
- Избегай synchronized: используй concurrent коллекции
- Думай об ограничениях: если задача 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 * количество ядер