Ваш код создает огромное количество ForkJoinPool
экземпляров и никогда не вызывает shutdown()
ни в одном пуле после его использования. Поскольку в случае Java 8 ничто в спецификации не гарантирует прекращения рабочих потоков, этот код может даже закончиться с 2000 (⟨ число пулов ⟩) раз ⟨ количество ядер ⟩ Темы.
На практике наблюдаемое поведение проистекает из недокументированного времени простоя , равного двум секундам. Обратите внимание, что согласно комментарию, следствием истекшего тайм-аута является попытка сократить количество работников , что отличается от просто прекращения работы. Таким образом, если поток n испытывает таймаут, не все потоки n завершаются, но число потоков уменьшается на один, а остальные потоки могут снова ждать. Кроме того, фраза «начальное значение времени ожидания» уже намекает на это, фактическое время ожидания увеличивается каждый раз, когда это происходит. Таким образом, требуется <em>n</em> * (<em>n</em> + 1)
секунд для n незанятого рабочего потока для завершения из-за этого (недокументированного) тайм-аута.
Начиная с Java 9, существует настраиваемый keepAliveTime , который можно указать в новом конструкторе из ForkJoinPool
, который также документирует значение по умолчанию:
keepAliveTime
- истекшее время с момента последнего использования до завершения потока (а затем позднее, при необходимости, заменено). Для значения по умолчанию используйте
60, TimeUnit.SECONDS
.
Эта документация может ввести в заблуждение, полагая, что теперь все рабочие потоки могут завершаться вместе, когда простаивают для keepAliveTime , но на самом деле все еще существует поведение сокращения пула только по одному за раз, хотя сейчас Время не увеличивается. Так что теперь для завершения n простаивающего рабочего потока может потребоваться до 60 * <em>n</em>
секунд. Поскольку предыдущее поведение не было определено, это даже не несовместимость.
Следует подчеркнуть, что даже при одном и том же поведении тайм-аута результирующее максимальное количество потоков может измениться, например, когда более новая JVM с лучшей оптимизацией кода сокращает время выполнения реальных операций (без искусственных вставок Thread.sleep(…)
) это будет создавать новые потоки быстрее, в то время как завершение все еще связано со временем настенных часов.
Вывод заключается в том, что вам никогда не следует полагаться на автоматическое завершение рабочего потока, если вы знаете, что пул потоков больше не нужен. Вместо этого вам следует позвонить shutdown()
, когда вы закончите.
Вы можете проверить поведение с помощью следующего кода:
int threadNumber = 8;
ForkJoinPool pool = new ForkJoinPool(threadNumber);
// force the creation of all worker threads
pool.invokeAll(Collections.nCopies(threadNumber*2, () -> { Thread.sleep(500); return ""; }));
int oldNum = pool.getPoolSize();
System.out.println(oldNum+" threads; waiting for dying threads");
long t0 = System.nanoTime();
while(oldNum > 0) {
while(pool.getPoolSize()==oldNum)
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(200));
long t1 = System.nanoTime();
oldNum = pool.getPoolSize();
System.out.println(threadNumber-oldNum+" threads terminated after "
+TimeUnit.NANOSECONDS.toSeconds(t1 - t0)+"s");
}
Java 8:
8 threads; waiting for dying threads
1 threads terminated after 2s
2 threads terminated after 6s
3 threads terminated after 12s
4 threads terminated after 20s
5 threads terminated after 30s
6 threads terminated after 42s
7 threads terminated after 56s
8 threads terminated after 72s
Java 11:
8 threads; waiting for dying threads
1 threads terminated after 60s
2 threads terminated after 120s
3 threads terminated after 180s
4 threads terminated after 240s
5 threads terminated after 300s
6 threads terminated after 360s
7 threads terminated after 420s
Никогда не завершено, по-видимому, по крайней мере один последний рабочий поток остается живым