Потоки Java 8 - исключение stackoverflow

Запуск следующего примера кода заканчивается на:
" Исключение в потоке "main" java.lang.StackOverflowError"

import java.util.stream.IntStream;
import java.util.stream.Stream;

public class TestStream {

    public static void main(String[] args) {
        Stream<String> reducedStream = IntStream.range(0, 15000)
            .mapToObj(Abc::new)
            .reduce(
                Stream.of("Test")
                , (str , abc) -> abc.process(str)
                , (a , b) -> {throw new IllegalStateException();}
        );
        System.out.println(reducedStream.findFirst().get());
    }

    private static class Abc { 
        public Abc(int id) {
        }

        public Stream<String> process(Stream<String> batch) {
            return batch.map(this::doNothing);
        }

        private String doNothing(String test) {
            return test;
        }
    }
}

Что именно вызывает эту проблему? Какая часть этого кода является рекурсивной и почему?

Ответ 1

Ваш код не рекурсивно зацикливается. Вы можете протестировать с меньшими номерами для диапазона IntStream (то есть 1 или 100). В вашем случае это фактическое ограничение размера стека, которое вызывает проблему. Как отмечалось в некоторых комментариях, это то, как потоки являются процессами.

Каждый вызов в потоке создает новый поток обертки вокруг исходного. Метод findFirst() запрашивает предыдущий поток для элементов, который, в свою очередь, запрашивает предыдущий поток для элементов. Поскольку потоки не являются реальными контейнерами, а только указатели на элементы результата.

Взрыв оболочки происходит в накопителе методов уменьшения (str, abc) → abc.process(str). Реализация метода создает новую обертку потока на результат (str) предыдущей операции, подавая на следующую итерацию, создавая новую оболочку результата (результат (str))). Таким образом, механизм накопления является одной из обертки (рекурсии), а не приложения (итерации). Поэтому создание нового потока фактического (сплющенного) результата, а не ссылки на потенциальный результат, остановит взрыв, т.е.

public Stream<String> process(Stream<String> batch) {
        return Stream.of(batch.map(this::doNothing).collect(Collectors.joining()));
    }

Этот метод является всего лишь примером, поскольку ваш оригинальный пример не имеет никакого смысла, потому что он ничего не делает, и этот пример тоже. Это просто иллюстрация. Он в основном выравнивает элементы потока, возвращаемого методом карты, в одну строку и создает новый поток на этой конкретной строке, а не на самом потоке, это разница с исходным кодом.

Вы можете настроить stacksize с помощью параметра -Xss, который определяет размер стека для потока. Значение по умолчанию зависит от платформы, см. Также этот вопрос "Какова максимальная глубина стека вызовов java?" Но будьте осторожны при увеличении, этот параметр применяется ко всем потоки.