Конструкции типа while(true) или for(;;) довольно опасные. Когда люди их пишут, то всегда надеются на то, что сработает условие выхода из цикла. Однако, на самом деле, как бы мы ни были уверены, такие конструкции лучше избегать.

В любом программном продукте есть баги, и те инварианты, на которые идёт расчёт, могут просто не сработать. Решение? Читайте в переводе от команды Spring АйО, где автор указывает примеры того, как с этими конструкциями поступили в Jooq и как с ними работает один из департаментов University of California.


Мудрый человек однажды сказал:

Всё, что может пойти не так — пойдёт не так. — Мерфи

Некоторые программисты — мудрые люди, и один из них однажды сказал:

Хороший программист — это тот, кто смотрит по обеим сторонам, переходя улицу с односторонним движением. — Даг Линдер

В идеальном мире всё работает как положено, и может показаться разумным просто потреблять данные до самого конца. Поэтому подобный шаблон встречается повсеместно в любом коде:

for (;;) {
    // что-то
}
while (1) {
    // что-то
}
10 что-то  
20 GOTO 10

Хотите доказательств? Поиск по GitHub по запросу while(true) даёт тысячи совпадений: https://github.com/search?q=while+true&type=Code

Никогда не используйте потенциально бесконечные циклы

В информатике есть интересная проблема под названием «Проблема остановки» / “The Halting Problem”. Одна из особенностей данной проблемы заключается в том, что Алан Тьюринг много лет назад доказал, что эта проблема неразрешима. Со стороны и с высоты опыта нам, разработчикам, часто кажется, что это не так и мы всё контролируем.

Комментарий от эксперта Spring АйО, Михаила Поливахи

Здесь я быстро поясню о сути проблемы. Представим, что у нас есть программа, и эта программа принимает какие-либо входные данные. Абстрагируемся сейчас от конкретного языка программирования и других деталей.

И вот теперь давайте попробуем ответить на вопрос: можем ли мы написать такой статический анализатор (по сути другую программу, например PMD или spotbugs), который бы посмотрел на пару: программу и входные данные для неё и дал бы точный ответ - завершиться ли в целом исполнение данного фрагмента программы или уйдет в бесконечный цикл? Это и есть суть проблемы.

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

Например, можно быстро понять, что выполнение следующего кода  будет бесконечным:

for (;;) continue;

…а вот эта программа завершится всегда:

for (;;) break;

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

Учимся на практике

В jOOQ недавно познакомились с проблемой остановки на собственном опыте. До исправления тикета #3696, мы использовали обходной путь для бага (или недоработки) в JDBC-драйвере SQL Server.

Эта ошибка приводила к тому, что цепочки исключений SQLException не сообщались корректно, например, когда следующий триггер выбрасывает несколько ошибок:

CREATE TRIGGER Employee_Upd_2 ON EMPLOYEE FOR UPDATE
AS
BEGIN
    RAISERROR('Employee_Upd_2 Trigger called...',16,-1)
    RAISERROR('Employee_Upd_2 Trigger called...1',16,-1)
    RAISERROR('Employee_Upd_2 Trigger called...2',16,-1)
    RAISERROR('Employee_Upd_2 Trigger called...3',16,-1)
    RAISERROR('Employee_Upd_2 Trigger called...4',16,-1)
    RAISERROR('Employee_Upd_2 Trigger called...5',16,-1)
END
GO

Чтобы поведение было единым для всех СУБД, мы явно «потребляли» эти исключения:

consumeLoop: for (;;) {
    try {
        if (!stmt.getMoreResults() && 
             stmt.getUpdateCount() == -1)
            break consumeLoop;
    }
    catch (SQLException e) {
        previous.setNextException(e);
        previous = e;
    }
}

Для большинства наших клиентов это работало, потому что цепочка исключений, как правило, конечна и небольшая. Даже в приведённом выше примере количество ошибок часто варьируется от 1 до 5. Я только что сказал… «как правило»?

Как уже было сказано выше: их может быть от 1 до 5. Но может быть и 1000. Или миллион. Или, что ещё хуже — бесконечное количество. Именно это случилось в тиките #3696, когда один из клиентов использовал jOOQ с SQL Azure.

В идеальном мире не может быть бесконечного количества SQLException, но наш мир далёк от идеала, и в SQL Azure тоже был баг (возможно, всё ещё есть), который снова и снова выбрасывал одну и ту же ошибку. Это привело к OutOfMemoryError, поскольку jOOQ создавал огромную цепочку исключений. Это всё же лучше, чем бесконечный цикл — по крайней мере, исключение легко отследить и обойти. А вот бесконечный цикл мог бы полностью заблокировать сервер для всех пользователей клиента.

Исправление выглядит теперь так:

consumeLoop: for (int i = 0; i < 256; i++) {
    try {
        if (!stmt.getMoreResults() && 
             stmt.getUpdateCount() == -1)
            break consumeLoop;
    }
    catch (SQLException e) {
        previous.setNextException(e);
        previous = e;
    }
}

В духе популярного высказывания:

640 КБ должно хватить всем

Комментарий от эксперта Spring АйО, Михаила Поливахи

Речь про фразу, которую часто приписывают Биллу Гейтсу: "640K ought to be enough for anybody". Она олицетворяет предположение, в котором ранее были уверены на 100%, которое, как показало будущее, было совершенно неверно

Единственное исключение

Как показал этот неловкий пример — всё, что может пойти не так, обязательно пойдёт не так. В контексте потенциально бесконечных циклов важно понимать, что такая ошибка может вывести из строя весь сервер.

В Лаборатории реактивного движения (Jet Propulsion Laboratory) при Калифорнийском технологическом институте эту проблему специально адресовали в гайдлайнах для написания кода:

Правило 3 (границы циклов):
Все циклы должны иметь статически определимую верхнюю границу на количество итераций. Статический анализатор должен быть в состоянии подтвердить существование этой границы.

Комментарий от эксперта Spring АйО, Михаила Поливахи

Речь не о том, что граница цикла всегда является константой на этапе компиляции, такая как 10 или 256, речь о том, что она в целом есть, и конструкции типа for (int i = 0;; i+=1) и их вариации являются запрещенными.

Для более четкого понимания и кому интересно, как выглядят coding standards департамента в UCLA, где пишется код для Nasa, то вот ссылка: https://yurichev.com/mirrors/C/JPL_Coding_Standard_C.pdf

Или более конкретно для Java: https://www.havelund.com/Publications/jpl-java-standard.pdf

Допускается одно исключение: непрерываемый цикл на поток или задачу, в котором обрабатываются входящие запросы на сервер. Такой серверный цикл должен быть помечен комментарием:

 /* @non-terminating@ */

Иными словами, за очень редкими исключениями, никогда не допускайте в своём коде возможность бесконечных циклов без ограничения количества итераций (то же самое, кстати, касается рекурсии).

Заключение

Пройдитесь по своему коду уже сегодня и найдите все случаи while (true), for (;;) или do {} while (true);. Внимательно проверьте, могут ли они завершиться — с помощью break, throw, return или continue для внешнего цикла.

Скорее всего, вы (или кто-то до вас) писали этот код с мыслью:

… да ладно, этого не произойдёт.

Потому что мы все знаем, что происходит, когда думаешь, что ничего не произойдёт.


Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

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


  1. tuxi
    05.09.2025 12:40

    Поиск по GitHub по запросу while(true) даёт тысячи совпадений

    А почему он не должен давать тысячи совпадений, если это типовая реализация метода run у интерфейса Runnable ???


    1. poxvuibr
      05.09.2025 12:40

      Не, типовая будет while(inProgress) . Но вообще сам поисковый запрос несколько безумен и количество результатов мало о чём говорит. while false даст их всего в два раза меньше


      1. tuxi
        05.09.2025 12:40

        Нам нужен бесконечный цикл, отработал, поспал, опять запустился. И ловим InterruptedException если в основном потоке было решено прибить эту фихню.


  1. randomsimplenumber
    05.09.2025 12:40

    Как по мне, исправление напоминает костыль. 256 итераций. Почему не 8? Или не 32768?


    1. poxvuibr
      05.09.2025 12:40

      Оно неполное. Надо кидать эксепшн, если цикл зашёл за 256, тогда будет лучше. А цифра, конечно, произвольно выбирается. Какая - то цифра за которую цикл точно не должен зайти.


  1. aol-nnov
    05.09.2025 12:40

    Вам не кажется, что перевод статей 2015 года - это кризис жанра и надо как-то пересмотреть стратегию развития своего паблика?

    Не, я понимаю - есть "не стареющая классика", но, мяу?!


  1. buratino
    05.09.2025 12:40

    Никогда не используйте потенциально бесконечные циклы

    хрень полная.

    Любое встроенное ПО использует бесконечные циклы. Или в виде машины состояний или в ардуино стиле с setup() и loop(). И не обязательно встроенное.


  1. Wesha
    05.09.2025 12:40

    Хороший программист — это тот, кто смотрит по обеим сторонам, переходя улицу с односторонним движением.

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


    1. randomsimplenumber
      05.09.2025 12:40

      Глаза у человека находяться перед ушами, так что есть шанс увидеть автомобиль раньше чем услышать.


      1. Wesha
        05.09.2025 12:40

        Автомобиль, который можно раньше увидеть, чем услышать, оштрафуют за превышение скорости (звука).

        Анекдот (физический)"

        Летят две вороны с дозвуковой скоростью.
        — Забор!!!
        — Вижу!
        Шмяк!
        Шмяк!

        Летят две вороны со сверхзвуковой скоростью.
        — Забор!!!
        Шмяк!
        — Вижу!
        Шмяк!

        Летят две вороны с гиперзвуковой скоростью.
        Шмяк!
        Шмяк!
        — Вижу!
        — Забор!!!


    1. taenur
      05.09.2025 12:40

      Есть условно бесшумные - электрички те же, да на свежем снежке.. а так, у программиста в этот звуковой датчик давно встроен нейронный шумодав, для игнорирования внешних раздражителей, куда и профили автозвука давно внесены.