Нельзя доводить до бесконечности количество параллельных задач. Это касается не только вычислительных задач (что очевидно), но и ввода-вывода (не столь очевидно, тем не менее - факт). Надо как-то ограничить параллельность. Вариантов решения масса. Один из лучших вариантов для равномерного распределения нагрузки, например:
- Создается массив потоков фиксированного размера (не ThreadPoolExecutor).
- Все задания помещаются во входную ConcurrentLinkedQueue (или BlockingQueue, если задания поступают постепенно и очередное приходится ждать).
- Каждый поток достает очередное задание из очереди (завершается в случае если очередь пустая (для случая ConcurrentLinkedQueue)), выполняет задание, помещает результат в выходную BlockingQueue.
- Вызывающий поток в цикле делает take() из выходной очереди столько раз, сколько задач положил во входную, после окончания цикла - обработка окончена.
Количество параллельных потоков подбирается эмпирически (в зависимости от ширины канала, в случае сетевого ввода/вывода).
Преимущества данного подхода: в каждый момент времени (за исключением периода завершения, когда входная очередь уже пуста) выполняется ровно N задач, не зависимо от продолжительности выполнения каждой отдельной задачи (продолжительность долгих и коротких задач может отличатся в десятки/сотни раз).