Кастомный пул потоков на Java с настраиваемыми очередями и параметрами. Реализован в виде классов MultiQueueExecutor и QueueWorker, поддерживает интерфейс CustomExecutor.
- Round-Robin распределение задач по нескольким очередям.
- Настраиваемые параметры пула:
corePoolSize,maxPoolSize,queueSize,keepAliveTime,minSpareThreads. - Интерфейс
CustomExecutor(execute,submit,shutdown,shutdownNow). - Обработка отказов: выброс
RejectedExecutionExceptionпри переполнении очереди. - Логирование ключевых событий: создание/запуск/завершение потоков, приём и выполнение задач, таймаут бездействия.
- Graceful shutdown: мягкое и принудительное завершение воркеров.
- Java 21
- Maven
- SLF4J API + Logback
-
Склонировать репозиторий:
git clone https://github.com/mephi-learn/dzthreadswork cd dzthreadswork -
Собрать проект:
mvn clean install
-
Запустить демонстрационную программу:
mvn exec:java -Dexec.mainClass="dzthreadwork.App"
Запуск App.java демонстрирует базовую работу пула с 100 задачами, по умолчанию с параметрами:
corePoolSize = 4
maxPoolSize = 8
queueSize = 10
keepAliveTime= 5 секунд
minSpareThreads = 2
В консоли выводятся логи о приёме и выполнении задач, а также итоговая статистика: число выполненных/отклонённых задач и среднее время на задачу.
Для проведения анализа производительности мы использовали два подхода:
- Демонстрационный тест в
App.java(100 задач,sleep(10ms)между отправками). - Бенчмарк в
ThreadPoolBenchmark.java(200 задач,sleep(10ms), аналогичные параметры).
| Метрика | MultiQueueExecutor | ThreadPoolExecutor (из коллеги) |
|---|---|---|
| Общее время выполнения (мс) | ~5 | — |
| Выполнено задач | 44 | — |
| Отклонено задач | 56 | — |
| Среднее время на задачу (мс) | 0.11 | — |
В тесте с 100 задачами наш пул выполнил 44 задачи и отклонил 56, обеспечив среднее время 0.11 мс. Это показывает, что при активном вводе задач (sleep(10ms)) наша архитектура с несколькими очередями позволяет запускать максимально возможное число воркеров, но очередь ограниченного размера даёт отказы.
Тест с 200 задачами и параметрами core=4, max=10, queueSize=10, keepAlive=5s, minSpareThreads=2:
| Метрика | MultiQueueExecutor | ThreadPoolExecutor |
|---|---|---|
| Время выполнения (мс) | 1584 | 1569 |
| Выполнено задач | 72 | 52 |
| Отклонено задач | 128 | 148 |
| Среднее время на задачу (мс) | 22 | 30.17 |
Несмотря на чуть большее общее время, наш пул выполнил на 38% больше задач и сократил среднее время на задачу на 37% по сравнению со стандартным исполнителем.
Процент_времени = (1584 - 1569) / 1569 * 100% ≈ -0.95% (медленнее)
Процент_задач = (72 - 52) / 52 * 100% ≈ 38.46% (больше задач)
Процент_среднего = (30.17 - 22) / 22 * 100% ≈ 37.13% (быстрее по задаче)
Среднее превосходство ≈ 24.89%
Для выявления оптимальных значений ключевых параметров мы провели серию тестов (200 задач, keepAliveTime=10s, sleep(10ms) между задачами):
| queueSize | Время (мс) | Выполнено | Отклонено | Среднее (мс) |
|---|---|---|---|---|
| 2 | 10 | 28 | 172 | 0.36 |
| 10 | 11 | 92 | 108 | 0.12 |
| 25 | 11 | 172 | 28 | 0.06 |
| 35 | 10 | 200 | 0 | 0.05 |
| 50 | 9 | 200 | 0 | 0.045 |
Вывод: слишком малая очередь приводит к большому числу отказов; оптимально — 25–35.
| maxPoolSize | Время (мс) | Выполнено | Отклонено | Среднее (мс) |
|---|---|---|---|---|
| 6 | 10 | 158 | 42 | 0.063 |
| 7 | 11 | 185 | 15 | 0.059 |
| 8 | 11 | 185 | 15 | 0.059 |
Вывод: значение вдвое больше числа ядер (8) даёт максимальную пропускную способность.
| corePoolSize | Время (мс) | Выполнено | Отклонено | Среднее (мс) |
|---|---|---|---|---|
| 2 | 11 | 200 | 0 | 0.055 |
| 3 | 11 | 200 | 0 | 0.055 |
| 4 | 10 | 200 | 0 | 0.05 |
Вывод: corePoolSize влияет мало при достаточном maxPoolSize, но рекомендуется устанавливать 2–4.
- Round-Robin: очереди задач выбираются циклически.
- Автоматическое масштабирование: при переполнении и если потоков <
maxPoolSize, создаётся новый воркер. - Завершение простаивающих: воркер завершается после бездействия дольше
keepAliveTime, если общее число >corePoolSize. - Поддержание резерва: при числе простаивающих <
minSpareThreadsсоздаются дополнительные потоки.