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

Сколько бы потоков создал в пуле?

2.4 Senior🔥 61 комментариев
#Многопоточность

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

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

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

Размер пула потоков (Thread Pool Sizing)

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

Типы задач

1. CPU-bound задачи (вычисления)

Примеры: обработка данных, криптография, алгоритмы

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CPUBoundPool {
    public static void main(String[] args) {
        // Для CPU-bound: количество потоков = количество ядер
        int coreCount = Runtime.getRuntime().availableProcessors();
        System.out.println("Available cores: " + coreCount);
        
        ExecutorService executor = Executors.newFixedThreadPool(coreCount);
        
        // Пример: CPU-intensive задача
        for (int i = 0; i < 100; i++) {
            executor.submit(() -> {
                long sum = 0;
                for (long j = 0; j < 1_000_000_000; j++) {
                    sum += j; // Сильно нагружает CPU
                }
                System.out.println("Sum: " + sum);
            });
        }
        
        executor.shutdown();
    }
}

Формула: threadCount = availableProcessors()

Почему: Больше потоков = больше context switching = медленнее

2. I/O-bound задачи (сеть, БД, файлы)

Примеры: REST API, доступ к БД, чтение файлов

public class IOBoundPool {
    public static void main(String[] args) {
        // Для I/O-bound: больше потоков, чем ядер
        int coreCount = Runtime.getRuntime().availableProcessors();
        int ioThreadCount = coreCount * 2; // Правило большого пальца
        
        ExecutorService executor = Executors.newFixedThreadPool(ioThreadCount);
        
        // Пример: I/O интенсивная задача
        for (int i = 0; i < 1000; i++) {
            final int taskId = i;
            executor.submit(() -> {
                try {
                    // Имитируем I/O: сетевой запрос или БД
                    Thread.sleep(1000); // Ждём I/O ответа
                    System.out.println("Task " + taskId + " completed");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        
        executor.shutdown();
    }
}

Формула: threadCount = availableProcessors() * 2 (или даже больше)

Почему: Пока один поток ждёт I/O, другие могут выполняться

Рекомендации для разных сценариев

API Server (REST контроллер)

Spring Boot автоматически конфигурирует, но вот как:

# application.yml
server:
  tomcat:
    threads:
      max: 200           # Макс потоков обработки (по умолчанию 200)
      min-spare: 10     # Мин потоков (по умолчанию 10)
      accept-count: 100 # Очередь запросов
@Configuration
public class ThreadPoolConfig {
    @Bean
    public ExecutorService taskExecutor() {
        int cores = Runtime.getRuntime().availableProcessors();
        return Executors.newFixedThreadPool(cores * 2);
    }
}

Формула для веб-сервера: 20-500 потоков (в зависимости от нагрузки)

Database Connection Pool

HikariCP (рекомендуемый):

spring:
  datasource:
    hikari:
      maximum-pool-size: 20      # Макс подключений
      minimum-idle: 5            # Мин подключений (idle)
      connection-timeout: 30000  # 30 сек
      idle-timeout: 600000       # 10 мин
@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:postgresql://localhost/db");
        config.setUsername("user");
        config.setPassword("pass");
        config.setMaximumPoolSize(20);
        config.setMinimumIdle(5);
        return new HikariDataSource(config);
    }
}

Формула: poolSize = ((coreCount * 2) + spindle_count)

Где spindle_count — количество жёстких дисков (для базы данных)

Async Task Processing

Для фоновых задач:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);           // базовый размер
        executor.setMaxPoolSize(50);            // максимум
        executor.setQueueCapacity(100);         // очередь перед увеличением
        executor.setThreadNamePrefix("Async-");
        executor.initialize();
        return executor;
    }
}

@Service
public class EmailService {
    @Async("taskExecutor")
    public void sendEmailAsync(String to, String subject) {
        // Отправляется в отдельном потоке
        mailService.send(to, subject);
    }
}

Рекомендации:

  • Core Pool Size: 10-50 (зависит от нагрузки)
  • Max Pool Size: 2-3x Core Pool Size
  • Queue Capacity: зависит от памяти

Рекомендации для разных нагрузок

┌─────────────────┬────────┬────────────┬──────────────────┐
│ Тип приложения  │ Cores  │ Рекомендка │ Примечание       │
├─────────────────┼────────┼────────────┼──────────────────┤
│ REST API        │ 8      │ 50-200     │ Зависит от нагрузки│
│ Background Job  │ 8      │ 10-50      │ Меньше нужно      │
│ CPU compute     │ 8      │ 8          │ = Количество ядер │
│ Database        │ 8      │ 20-30      │ По HikariCP       │
│ WebSocket Server │ 8      │ 100-500    │ Много связей      │
└─────────────────┴────────┴────────────┴──────────────────┘

Как тестировать правильный размер

import org.junit.jupiter.api.Test;
import java.util.concurrent.*;
import java.time.Instant;

class ThreadPoolSizingTest {
    @Test
    void testOptimalPoolSize() throws InterruptedException {
        for (int poolSize = 1; poolSize <= 32; poolSize++) {
            ExecutorService executor = Executors.newFixedThreadPool(poolSize);
            
            Instant start = Instant.now();
            CountDownLatch latch = new CountDownLatch(1000);
            
            // Запускаем 1000 I/O-intensive задач
            for (int i = 0; i < 1000; i++) {
                executor.submit(() -> {
                    try {
                        Thread.sleep(100); // Имитация I/O
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    latch.countDown();
                });
            }
            
            latch.await();
            long duration = Instant.now().toEpochMilli() - start.toEpochMilli();
            
            System.out.println("Pool size " + poolSize + ": " + duration + "ms");
            executor.shutdown();
        }
    }
}

Результат: найдёшь, где время минимально — это оптимальный размер

Мониторинг пула в продакшене

@Configuration
public class ExecutorMonitoring {
    @Bean
    public Executor monitoredExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor() {
            @Override
            public void execute(Runnable task) {
                int poolSize = getPoolSize();
                int activeCount = getActiveCount();
                int queueSize = getThreadPoolExecutor().getQueue().size();
                
                if (queueSize > 0) {
                    logger.warn("Thread pool busy: active={}, pool={}, queue={}",
                        activeCount, poolSize, queueSize);
                }
                super.execute(task);
            }
        };
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.initialize();
        return executor;
    }
}

Ключевые выводы

  • CPU-bound: потоков = ядер процессора
  • I/O-bound: потоков = ядер * 2-4 (зависит от I/O задержки)
  • REST API: 50-200 потоков (настройка зависит от нагрузки)
  • Database Pool: 20-30 подключений (по HikariCP рекомендациям)
  • Тестируй под реальной нагрузкой — теория может отличаться
  • Мониторь очередь — если растёт, увеличивай pool size
  • Не создавай нить на каждый запрос — это разрушит сервер