Надежная остановка неотвечающего потока

Мне интересно, как остановить не отвечающий поток в Java, чтобы он действительно умер.

Прежде всего, я хорошо знаю, что Thread.stop() устарел и почему его не следует использовать; на эту тему уже есть много отличных ответов, ср. [1][2]. Итак, вопрос точнее в том, возможно ли технически убить поток, код которого не контролируется нами, но, возможно, является враждебным и не отвечает на прерывания.

В простейшем случае враждебный поток будет работать while(true);, но он также может использовать память или другие системные ресурсы, чтобы причинить больше вреда. Вызов interrupt() в этом потоке явно неэффективен. Как насчет того, чтобы вместо этого позвонить stop()?

Я запустил это в отладчике, и на самом деле поток действительно исчезает. Но надежен ли этот подход? На этот случай можно было бы подготовить враждебную нить; подумайте о try{run();}catch(ThreadDeath t){run();}, где он ловит ThreadDeath, который создается, когда мы вызываем stop(), и снова рекурсивно вызывает сам себя.

Как сторонний наблюдатель мы не можем видеть, что происходит; Thread.stop() всегда работает бесшумно. Хуже всего то, что обычная диагностика больше не работает (пробовал это при отладке на Corretto 1.8.0_275 Windows x64): Thread.getState() всегда возвращает RUNNABLE независимо от успеха в уничтожении потока, то же самое касается Thread.isAlive() (всегда верно).


person pxcv7r    schedule 30.12.2020    source источник
comment
Если вы выполняете ненадежный код в потоке, могут возникнуть более серьезные проблемы, чем просто DOS.   -  person Henry    schedule 30.12.2020
comment
Большинство, если не все реализации jvm используют потоки системного уровня. Можно ли позволить ОС убить поток?   -  person Felix    schedule 28.01.2021
comment
Нет, если бы вы вытащили ковер из-под ног JVM, просто убив поток, то вы потенциально оставили бы кучу Java и другое состояние, такое как блокировки, в небезопасном состоянии.   -  person Neil Coffey    schedule 29.01.2021
comment
Вот несколько советов: stackoverflow.com/q/502218/829571   -  person assylias    schedule 29.01.2021
comment
Интересные и хорошие ответы, в частности, на локальные менеджеры безопасности. Однако часть об уничтожении потока все еще неясна.   -  person pxcv7r    schedule 30.01.2021
comment
Согласно моим экспериментам, isAlive() всегда возвращает true, если поток не завершился нормально (сам по себе или по прерыванию), как написано выше. Таким образом, этот цикл будет просто зацикливаться навсегда.   -  person pxcv7r    schedule 31.01.2021
comment
@matt, я имел в виду «вне потока», но внутри той же JVM. Я предполагаю, что мы запустили поток, но не знаем, что именно он выполняет.   -  person pxcv7r    schedule 31.01.2021


Ответы (5)


Это может быть невозможно, по крайней мере, не надежно в каждом сценарии.

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

В таком сценарии вам нужно убить процесс JVM, а не поток.

Немного дополнительного чтения:

Как получить стеки Java, когда JVM не может добраться до безопасной точки

Подсчитанные циклы

person Vlad L    schedule 30.12.2020

Короче говоря, нет 100% надежного способа остановить Thread так, как вам хочется.


Почему?

Это объяснение для тех, кто не знает почему. Любой, кто знаком с проблемой, может это пропустить.

Способ, которым потоки предназначены для принудительного завершения, заключается в состоянии прерывания Thread. Thread должен быть завершен с его < вызывается метод href="https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#interrupt()" rel="nofollow noreferrer">interrupt(), который устанавливает логический флаг в true.

Когда флаг прерывания установлен на true, Thread должен завершаться практически без задержки.

В любом случае Thread может просто проигнорировать это и продолжить работу.

Это когда stop() может быть вызван метод, который принудительно завершает работу Thread. Проблема в том, что этот метод портит параллелизм, может повредить объекты, и программа может быть повреждена без предупреждения пользователя. См. Почему метод stop() устарел?


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

Например, эти проблемы может вызвать враждебная третья сторона .jar, которая содержит Thread, который отказывается завершать работу.


Быстро и грязно

Это решение не является полностью безопасным, но в зависимости от использования оно может быть приемлемым, если вам действительно не нравится безопасность.

Попробуйте сначала вызвать метод interrupt() для Thread и дать ему немного времени для завершения.

Если Thread не отвечает, вы можете:

  • завершите программу и предупредите пользователя, чтобы он больше не запускал этот Thread.
  • stop() нить и надеяться на лучшее.

Сложный и безопасный

Самым безопасным решением, которое я могу придумать, является создание совершенно нового процесса для запуска Thread. Если Thread не хочет завершаться после interrupt(), вы можете просто завершить процесс с помощью System.exit(-1) и позволить ОС справиться с этим.

Вам нужно Впереход Pпроцесс Cсвязи для связи с другим процессом, что делает его намного сложнее, но и безопаснее.


Связанный

Как убить поток в Java?

Что такое InterruptedException в Java? (Отказ от ответственности: я уже ответил)

Что делает java.lang.Thread.interrupt()?

person fuggerjaki61    schedule 30.01.2021

Для меня isAlive возвращает false, если процесс завершается из-за Thread.stop.

Я сделал следующий пример, и он успешно убивает ошибочный поток.

import java.util.Arrays;
public class BrokenThreads{
    static boolean[] v = { true };
    public static void call(){
            try{
                while(true){
                    Thread.sleep(200);
                }
            
            } catch ( Throwable td){
                System.out.println("restarting");
                call();
            }   
    }
    public static void main(String[] args) throws Exception{
        
        Thread a = new Thread( BrokenThreads::call);
        
        a.start();
        Thread.sleep(500);
        System.out.println( Arrays.toString( a.getStackTrace() ) );
        while(v[0]){
            a.stop();
            System.out.println(a.getStackTrace().length);
            v[0] = a.isAlive();
        }
        System.out.println("finished normally");
        System.out.println( Arrays.toString( a.getStackTrace() ) );
    }
}

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

Это использует два метода, чтобы увидеть, остановился ли поток. isAlive и глубину трассировки стека.

person matt    schedule 31.01.2021

Я думаю, что вопрос описывает сценарий, который является причиной того, что Thread.stop() устарел с незапамятных времен, но еще не был удален … просто чтобы иметь «последний вариант», который следует использовать только в действительно отчаянных случаях и зная обо всем негативном влияние.

Но этот вызов Thread.stop() должен быть каким-то образом встроен в код, как и любая альтернатива, о которой можно подумать, так почему бы просто не исправить код для потока? Или, если это невозможно, потому что этот код поставляется со сторонней библиотекой без исходного кода, почему бы вместо этого не заменить эту библиотеку?

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

Но в производстве никогда не должно быть потока, который не останавливается при получении прерывания. Таким образом, не должно быть необходимости в замене Thread.stop().

person tquadrat    schedule 31.01.2021

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

Что вы можете сделать, так это изолировать поток, запустив на нем .finalize(), а затем заставить JVM выполнять операции GC, такие как Runtime.gc(), System.runFinalization(), при этом принудительно прерывая этот конкретный поток, чтобы обойти его поведение при воскрешении.

Я думаю, что .finalize() фактически устарел с java11 или, может быть, раньше, так что, вероятно, это вам не сильно поможет.

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

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

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

person EverNight    schedule 27.01.2021
comment
Я, вероятно, должен указать на тот факт, что я говорю об опыте работы с такого рода вещами с точки зрения кого-то, кто экспериментировал с завершением и сборкой мусора для целей, отличных от уничтожения определенных потоков. - person EverNight; 27.01.2021
comment
Вопрос не в том, как отслеживать/предотвращать изменения в памяти. Речь идет о завершении потока. - person pxcv7r; 28.01.2021
comment
Что вы имеете в виду под изолировать нить? Это может быть ближе к делу. Как вызов GC повлияет на время выполнения потока? Он может удалять ссылки из кучи (максимум). Принудительно прерывать — вот в чем вопрос; Как бы Вы это сделали? - person pxcv7r; 28.01.2021
comment
Правильно, поэтому, изолируя поток, я имею в виду поиск способа для вашей JVM обнаруживать внутри него вещи, которых там не должно быть... например, этот поток. Если вы найдете способ сделать это без мониторинга... тогда будьте моим гостем. Вторая часть заключается в эффективном уничтожении инвазивного эффекта. то есть что-то, что работает try{run();}catch(ThreadDeath t){run();} . Поскольку все непримитивные типы должны расширять класс Object со встроенным методом .finalize(), вы можете воспользоваться этим преимуществом, используя отражение, чтобы периодически вызывать его в своем собственном потоке врача. - person EverNight; 30.01.2021
comment
Сборка мусора выполняется в несколько этапов. 1. помечает конкретное адресное пространство как GC'd 2. в конечном итоге запускает фактический процесс сбора. Эти два шага относительно не связаны по ряду причин, в которые я не буду вдаваться, но основная идея принудительной финализации и сборки мусора состоит в том, чтобы по существу противодействовать этому идиотскому 'try{run();}catch(ThreadDeath t){run( );}' выполнение. - person EverNight; 30.01.2021
comment
скажем, вы реализуете что-то, что обнаруживает существование этого инвазивного потока. Теперь вам нужно разобраться с этим. Поскольку вы утверждаете, что прерывание ублюдка не работает, потому что он сам вызывает свою собственную директиву .run(), идея состоит в том, чтобы по существу избавиться от всего объекта вместо того, чтобы полагаться на функциональность, встроенную в сам объект. Таким образом, порождение другого потока, который вызывает .interrupt(), sleep(), .finalize() с использованием отражения, а также запрос Runtime.gc() сразу после этого, должно по существу убить его на более глубоком уровне, чем Thread.stop(). - person EverNight; 30.01.2021
comment
проще говоря: создайте еще один поток while (true), заставьте его вызывать все, что вы можете придумать, чтобы остановить ублюдочный поток от запуска кода в jvm, при этом вы также вызываете .finalize и Runtime.GC и все, что вы можно придумать, чтобы уничтожить сам Объект. Поток расширяет объект, понятно? Подумайте об этом таким образом. Вы не можете заразить свой компьютер вирусом, если вы не запускаете какое-либо вредоносное ПО. С помощью .finalize и gc вы, по сути, говорите антивирусу делать свое дело, то есть удалять вредоносное ПО. - person EverNight; 30.01.2021
comment
Наконец, потому что я один с этим вопросом. Вы должны существенно ускорить процесс сборки мусора. Это может отличаться в зависимости от ряда факторов, и, скорее всего, Runtime.GC() будет недостаточно. (это на моей машине для определенных целей, кроме работы с детскими потоками скрипта). Независимо от того, нужно ли вам реализовывать System.runFinalization(), Runtime.GC()... customForceFinalizationMethodNameHere() и т. д., зависит от ваших навыков, зависимостей и ноу-хау. - person EverNight; 30.01.2021
comment
Сборка мусора не уничтожит работающий поток. Работающий поток — это корень GC. Так что доработка ничего не решит. - person Stephen C; 31.01.2021
comment
Если бы вы прочитали 10% того, что я написал, вы бы поняли, что весь смысл был в том, чтобы заставить этот поток находиться в неработающем состоянии и вызывать GC и финализацию в этом состоянии, тем самым предотвращая его поведение обработчика исключений. . - person EverNight; 02.02.2021