Java. Остановись задача

Вот уже почти год как усиленно занимаюсь коддингом на Java. Столкнулся с довольно серьезной на мой взгляд проблемой, связанных с многопоточностью, как мне кажется, неразрешимой в рамках текущей реализации JVM от Oracle (сказанное относится к JDK 1.5 и выше). Дело в том, что на данный момент в Java нет возможности гарантированно безопасно остановить выполнение какого-либо потока. Данный пост разъясняет почему это именно так и предлагает начать дискуссию о способах решения этой проблемы.

Казалось бы тривиальная задача: имеем некий Thread (поток), который, мы точно знаем, безнадежно завис (может зациклился, может, что-то еще), при этом потребляет некоторые ресурсы. Что нам с ним делать? Хотелось бы ресурсы наши освободить. Казалось бы, что проще? Ан нет, при детальном изучении вопроса оказалось, что в JVM просто нет инструкции, чтобы корректно остановить зависший Thread. Старый метод Thread.stop() объявлен Deprecated и предан строжайшей анафеме. Как сказано в javadoc'е это метод «по сути небезопасен». Ну что же, если не безопасен не будем его использовать, дайте нам другой, безопасный метод. Но другого безопасного как ни странно не предлагается. Предлагается, безусловно, очень безопасная инструкция Thread.interrupt(). Но безопасна она, к сожалению, потому, что ровным счетом ничего не делает! Это всего лишь сообщение потоку: «Пожалуйста, остановись». Но если поток данное сообщение проигнорировал то… как сказано в документации «Если поток не отвечает на Thread.interrupt() вы можете использовать специфические для вашего приложению трюки». Спасибо, что разрешили. Что называется, крутись как хочешь.

Все становится еще сложней, если задача запущена в пуле потоков, через, например, ExecutorService.submit(Runnable). При этом мы даже не знаем, в каком именно потоке данная задача выполняется и уже не может применить даже запрещенный Thread.stop(). С другой стороны, мы имеем ссылку на Future, а у Future есть метод Future.cancel(boolean), который должен отменить выполнение задачи. Но если задача уже начала выполняться, вызов Future.cancel(true) на самом деле не остановит ее. В недрах реализации FutureTask выполняется код:

if (mayInterruptIfRunning) {
Thread r = runner;
if (r != null)
r.interrupt(); }

Т.е. опять потоку, в котором выполняется задача, всего лишь рекомендуется прекратить выполнение. К тому же, мы не имеем даже возможности узнать выполняется ли задача в данный момент или нет. Есть, вроде, метод Future.isDone(), но опять мимо, он возвращает true не только когда задача завершила выполнение, а сразу после вызова Future.cancel(), даже если задача все еще выполняется (ведь Future.cancel(true) не останавливает задачу которая уже начала выполняться).

Хорошо, если мы сами пишем весь код, тогда можно в нужных местах аккуратно обрабатывать Thread.isInterrupted() и все будет ОК. Но если мы запускаем сторонний код? Если у нас есть сервер расширяемый с помощью плагинов? Какой-нибудь криво написанный плагин может запросто привести к неработоспособному состоянию весь сервер ведь мы не можем корректно прервать выполнение зависшего плагина.

Признаюсь, я не знаю удовлетворительного решения. Может быть, метод Thread.stop() не так уж опасен? Очень хотелось бы услышать мнения Java программистов практиков по этому поводу.
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 28
  • +4
    Мне кажется, что если поток завис в managed-коде, то при вызове stop() ничего фатального не произойдет. Хотя зависание в managed-коде это грубая ошибка программы, и в этом месте лучше бы её исправить как-то иначе. А вот если поток передал управление операционной системе и вызов завис в unmanaged-коде, то stop() может привести к краху приложения. В дотнете имеются схожие проблемы, и там вызов Thread.Abort не работает для потоков, выполняющих в момент аборта нативный код, поскольку генерация ThreadAbortException производится только в контексте управляемого кода, и если поток зависнет в нативном коде, его можно будет прибить только вызовом нативной функции Win32, что очень небезопасно. Думаю, в java все устроено примерно так же.
    • 0
      А такой вариант не катит?

      private boolean stopped = false;

      public void run() {
      while (!stoped) {
      // code here
      }
      }

      public void stop () {
      stopped = true;
      }
      • 0
        Автор говорит в первую очередь о проблеме контроля за модулями, которые написаны другими разработчиками. Они могут написать корявый код, который будет зависать. А сервер приложения ничего не сможет с этим сделать.
        • +3
          Именно так и работает interrupt():
          while (!interrupted()) {
          // code here
          }
          • 0
            только с volatile, если уж на то пошло. то что Вы написали работать не будет.
          • +12
            1) Как вы определите что поток именно «завис» а не выполняет полезную работу? О проблеме остановки машины Тьюринга слышали? Я скажу вам — никак не попределишь. Это фундаметальная проблема которая неразрешима.

            2) Никто не пишет jVM — все пишут JVM.

            3) Вы явно путаете понятия виртуальной машины и библиотеки классов (Java Class Library/JDK). Говорить, что у JVM нет метода остановки потока — абсурд. Сказали бы уже тогда, что нет инструкции :)
            • +1
              Спасибо за дельные замечания
              1) Определять зависшие потоки можно по разному, в зависимости от того, какие задачи в этих потоках выполняются. В общем случае можно отслеживать время, которое задачи запускаемые в отдельных потоках выполняются и при превышении некоторого лимита выполнять конструкцию Thread.interrupt() и, если Thread.isAlive() по прежнему возвращает true считать поток зависшим.
              2) Исправил
              3) Исправил
            • +4
              Может быть, метод Thread.stop() не так уж опасен?

              Хм. Это сложный вопрос. Что такое «опасен»? Зависший тред сразу показывает что что-то явно не так с приложением. А если тред убить — то ничего подобного не видно: ошибки могут проявляться не тут, не сейчас, и/или не всегда, и ловить их будет оочень весело. Собственно, все эти причины описаны в тех же жавадоках, как и методы относительно правильных решений этой проблемы: docs.oracle.com/javase/7/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html
              • +1
                Он опасен хотя бы тем, что после его вызова приложение переходит в неопределённое состояние.
                • 0
                  Да, именно прочтя эту доку я и создал тему. На очень животрепещущий вопрос: «What if a thread doesn't respond to Thread.interrupt? (Что делать если поток не откликается на Thread.interrupt)»? Там дается весьма пространный ответ: «In some cases, you can use application specific tricks», что я понимаю как «В некоторых случаях вы можете использовать специфические для вашего приложению трюки». Этот ответ никак нельзя назвать исчерпывающим.
                  • +3
                    На исчерпывающий ответ там претендует другая строка: «Unfortunately, there really isn't any technique that works in general». И это, увы, так: никак нельзя узнать что происходит с другим потоком. Может, он заблокировался на IO в нативном коде, и тогда stop() вообще ничего не даст. А может, он висит на мониторе, и тогда прерывание работы приведет вообще неизвестно к чему. Единственный случай когда может быть и можно рискнуть со stop() — это «вечный цикл», и то, надо держать пальцы крестиком и надеяться что вылетевший в случайном месте ThreadDeath не будет заглушен, корректно обойдет все finally блоки, освободит ресурсы, разблокирует мониторы…
                • 0
                  Подобная ситуация с потоками в Python: там поток даже на паузу поставить нельзя.
                  • +3
                    Вообще, я бы делал так:
                    1. Если каким-то образом удалось определить, что один из потоков «подвис», прибиваем всё приложение. В общем случае, как уже было сказано выше, нельзя определить, есть ли зависший поток, так что, может быть, спасёт какая-нибудь эвристика.
                    2. Чтобы приложение всё же продолжало работать дальше, делаем guard-процесс.

                    Чаще всего приложение — это какой-нибудь сервер, в котором потоки обслуживают входящие запросы. В этом случае совсем хорошо было бы сделать так. Рабочий процесс может с guard-процессом общаться каким-нибудь средством IPC (те же сокеты, например). Если рабочий процесс обнаруживает внутри себя зависший поток, он предпринимает следующие действия:
                    1. Отдаёт guard-процессу запустить и проинициализировать новый процесс, но пока не слушать порт (он ведь слушается рабочим процессом).
                    2. Освобождает порт и закрывает все соединения.
                    3. Отдаёт guard-процессу команду о том, что новый процесс может отныне слушать порт.
                    4. Рабочий процесс прибивает себя.
                    Так снижается время простоя сервера. Правда, получится, что одновременно будет запущено 3 процесса (из них 2 — рабочих), причём новый рабочий процесс вынужден будет выполнять, возможно дорогую, инициализацию в то время, пока старый рабочий процесс отъедает много ресурсов на зависшем потоке.

                    Сложно? А что хотели? Thread.stop() действительно небезопасен. Ведь все блокировки слетают и приложение остаётся в неконсистентном состоянии. Дальшейшее его повидение может быть непредсказуемым.

                    А плагины вот так просто тоже не ставятся на продуктивный сервер. Они должны быть «из доверенных источников». Да и вообще, что значит плагин для сервера? Кто будет делать запросы к этому плагину? Тогда нужно, чтобы и клиент знал про этот плагин и умел им пользоваться.
                    • +1
                      Язык Java создавался так, чтобы побудить программиста писать максимально безопасный код. Поэтому и были объявлены depricated всякие потенциально опасные операции типа stop(), чтобы заставить программиста более тщательно работать с потоками, а не надеятся на мухобойную stop(). Т.е. вопрос не в JCF, а в архитектуре самого языка. Вряд ли в этом будут какие-то изменения.
                      • 0
                        (очепятка, не JCF, и даже не JFC, а JDK)
                      • +2
                        Добавлю свои 5 копеек про Андроид:

                        метод Thread#stop в виртульной машине dalvik не поддерживается вообще. Я задавал вопрос на stackoverflow, но никакого толкового ответа не получил (предложили просто убить приложение) и использовал следующее "грязное" решение:

                        Если приложение не серверное(в смысле оно относительно часто перезапускается) и его выполнение критично не зависит от работы сторонней библиотеки в которой произошло зависание можно просто выставить потоку минимальный приоритет.

                        Очевидные минусы: работающая нить тратит системные ресурсы (например, при выполнении на мобильном устройстве это может привести к ускорению разряда батарее).
                        • +5
                          Это всего лишь сообщение потоку: «Пожалуйста, остановись»


                          Нуу, интеррупт заставит подключения или методы(wait(), sleep() и пр), которые его поддерживают, плюнуть эксепшн, который потом можно словить и спокойно выйти. То есть если поток ждет на оперциях ввода-вывода из файла, сокета или базы данных(вроде как они тоже это поддержиывают) то его можно впоне безопасно остановить. То же самое и если поток ждет на мониторе или CV.

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

                          • +2
                            Да, без сомнения все будет хорошо, если мы имеем дело с правильно написанным кодом. Но если в коде есть ошибка приводящая, например, к бесконечному циклу, то корректно прервать зависший в цикле поток возможности нет.
                            • 0
                              Имхо, в java архитектурно всё верно решено — в ущерб «простоте и удобству» (сомнительной нужности) остановки некорректного («зависшего») потока предпочли предсказуемость. Метод stop() в его реализации по определению потенциально не может привести ни к чему хорошему.
                              • +1
                                Ребята, я, видимо, что-то не понимаю, но разве в заголовке «Java. Остановись задача» не пропущена запятая? (Если что, прошу простить за дурацкий вопрос и объяснить, в чем ошибся…)
                                • –2
                                  Что на ум пришло: вроде бы здесь 'задача' не обращение, но запятую поставить хочеться)
                                  Нужно в справочник глянуть.
                                  • +5
                                    А если не обращение, то что по-вашему?
                                • +1
                                  Thread.stop() небезопасен в том плане, что перед уничтожением потока отпускаются все мониторы и блокировки. Таким образом, если поток совершал какие-либо действия над объектом в защищенном коде и не успел завершить логику — остальные потоки получат объект в непредсказуемом состоянии.
                                  По сути, такая ситуация возможна в любом коде, который плохо обрабатывает исключения.

                                  Thread.stop() все таки используется (вместе с ThreadDeath). Другого способа, кроме как постоянно смотреть флаг — нету.
                                  • +3
                                    Думаю, книга Java Concurrency in Practice (легко гуглится) поможет разобраться в ситуации. Целая глава там отведена описанию подходов для прекращения работы потока по требованию. Лично мое мнение — запуская сторонний код, нет вообще никаких гарантий его работоспособности в много-поточной среде.
                                    • 0
                                      Так а какое мнение? Да многопоточное программирование на потоках, мюьтексах и барьерах памяти сложно и опасно. Да в этой модели нет фозможности остановить код не спроектированный специально для остановки. Без существенного ограничения на то как используются ресурсы, такие как мьютексы и потоки ввода-вывода, я не представляю как можно это изменить.

                                      Бороться понятно как — писать код корретно обрабатывающий прерывание. Если сторонняя бибилотека этого не умеет — пишем свою. Или убийство процесса и watchdog, смотря какие требования.
                                      • +8
                                        Дам ссылку на свои размышления по схожему поводу (http://mantonov.blogspot.com/2011/10/blog-post.html), когда стоялаи задачи реализовать сервис по запуску untrusted кода.

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

                                        Автор плагина может сделать int[] array = new ing[1000000], и весь процесс моментально упадет с OutOfMemoryError, вы и пикнуть не успеете. Или же, в этом плагине может запустить рекурсивная функция без нормальной проверки условия выхода, и вы получаете stack overflow.

                                        Я долго не мог поверить, что это на самом деле так, и читал документацию по Java Security Platform Architecture — но в итоге все подтвердилось, модель безопасности Java не дает вам надежно защититься от сбоев, вызванных чрезмерным потреблением ресурсов.

                                        Самое нормальное решение в случае плагинов (если там действительно требуется серьезные усилия по безопасности) — это запускать весь код в отдельном процессе JVM (под ограниченными правами, выдавая ограниченное количество памяти), и дальше мониторить этот процесс, и убивать в случае превышения лимита по времени работы.
                                        • 0
                                          небольшое дополнение про

                                          Если у нас есть сервер расширяемый с помощью плагинов

                                          (application сервер ввиду имеется?)

                                          вот для того, чтобы память не кончилась, сокетов хватило, лок ресурса не завис и т.д. — подобные «низкоуровневые» операции делать на аппсервере _низя_ и аппсервер сам лучше знает, как этими ресурсами управлять.
                                        • 0
                                          Я вот думаю, что Thread.stop() зря сделали deprecated. При написании прикладного кода это, безусловно, очень вредная и опасная функциональность. Однако же для системного когда (например, код сервера приложений или, допустим, OSGi контейнера), это, фактически, единственный способ относительно надёжно пристрелить поток.

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

                                          В принципе, для выгрузки классов можно было бы предусмотреть механизм «форсированной» выгрузки, когда все ссылки на объекты выгруженных классов заменяются на специальное значение unloaded и вызывают исключение при обращении к ним. Я думаю, это можно было бы сделать практически без накладных расходов в рантайме (ну разве что сама операция форсированной выгрузки будет не очень быстрой).

                                          Для OutOfMemory можно было бы предусмотреть несколько изолированных «куч» с контролем границы между ними, но с учётом разделяемых данных это довольно сложно сделать.

                                          Можно рассуждать о том, что это всё проблема кривых приложений, и.т.д, и.т.п. Но факт в том, что программисты делают ошибки, и ничего с этим не поделаешь. Надо как-то это контролировать. Не только багрепортами, но и самим рантаймом. Вот, например, при разработке Erlang это учли, и повальная смерть процессов там — вполне нормальное явление. Конечно, там нет разделяемой памяти, это существенно упрощает дело.

                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.