24 февраля 2012 г.

java: Почему Thread.stop, Thread.suspend, Thread.resume и Runtime.runFinalizersOnExit объявлены deprecated?

Это я на скорую руку по надобности переводил доку оракловскую. Часть этих док я ещё на тысячах в аспирантуре переводил…
Оригинал (английский): Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated?
http://docs.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html

Почему не рекомендуется использовать Thread.stop?


Потому что это по своей сути небезопасно. Это приводит к разблокировке всех залоченных мониторов (исключение ThreadDeath пробрасывается вверх по стеку). Если любой из объектов, ранее защищенный мониторами, был в неустойчивом состоянии, таким его теперь могут увидеть другие потоки. Это может привести к неопределённому поведению, как скрытому труднообнаружимому, так и ярковыраженному. В отличие от других unchecked-исключений, ThreadDeath убивает потоки молча, так что пользователь может и не узнать, что его программа, вероятно, повреждена. Повреждение может проявиться в любое время после реальной аварии, через несколько часов или даже дней.

А я могу просто поймать исключение ThreadDeath и исправить поврежденный объект?


Теоретически, наверное, но это бы значительно усложнило задачу написания правильного многопоточного кода. Задача была бы почти непреодолимой по двум причинам:
1. Поток может бросить исключение ThreadDeath практически в любом месте. Все синхронизированные методы и блоки должны быть очень подробно изучены с учетом этого.
2. Поток может бросить второе исключение ThreadDeath при очистке от первого (в catch или finally). Очистку придётся повторять до достижения успеха. Код для обеспечения этого было бы довольно сложным.
Короче, это просто непрактично.

А что насчёт Thread.stop(Throwable)?


В дополнение ко всем описанным выше проблемам, этот метод может быть использован для генерации исключения, которое целевой поток не готов обработать (включая checked-исключения, которые поток вообще не может бросить без этого метода). Например, следующий метод ведёт себя аналогично операции Java throw, но обходит попытки компилятора гарантировать, что вызываемый метод объявил все checked-исключения, которые он может бросить:
static void sneakyThrow(Throwable t) {
        Thread.currentThread().stop(t);
    }

Что я должен использовать вместо Thread.stop?


Большинство использований stop может быть заменено кодом, который просто изменяет некоторую переменную, чтобы указать, что целевой поток должен прекратить работу. Целевой поток должен эту переменную регулярно проверять, и выйти из своего метода run в каком-то определённом порядке, если переменная указывает остановить работу. (Это подход, который рекомендуется всегда в разных Java Tutorial.) Для обеспечения оперативной связи стоп-запроса, переменная должна быть volatile (или доступ к переменной должен быть синхронизирован).

Для примера предположим, что ваш апплет содержит следующие методы start, stop и run:
private Thread blinker;

    public void start() {
        blinker = new Thread(this);
        blinker.start();
    }

    public void stop() {
        blinker.stop();  // UNSAFE!
    }

    public void run() {
        Thread thisThread = Thread.currentThread();
        while (true) {
            try {
                thisThread.sleep(interval);
            } catch (InterruptedException e){
            }
            repaint();
        }
    }
Вы можете избежать использования Thread.stop заменив методы stop и run апплета:
private volatile Thread blinker;

    public void stop() {
        blinker = null;
    }

    public void run() {
        Thread thisThread = Thread.currentThread();
        while (blinker == thisThread) {
            try {
                thisThread.sleep(interval);
            } catch (InterruptedException e){
            }
            repaint();
        }
    }

Как мне остановить поток, который ожидает в течение длительного времени (например, для ввода)?


Это то, для чего предназначен метод Thread.interrupt. Тот же «state based» механизм сигнализации показанный выше может быть использован, но изменение состояния (blinker = null, в предыдущем примере) может сопровождаться вызовом Thread.interrupt, чтобы прервать ожидание:
public void stop() {
        Thread moribund = waiter;
        waiter = null;
        moribund.interrupt();
    }
Для работы этого метода, важно, что любой метод, который ловит interrupt-исключения и не готов иметь дело с этим сразу же вновь бросает* исключение. Мы говорим, reasserts вместо rethrows, потому что это не всегда возможно выбросить исключение. Если метод, что бросается в InterruptedException не объявлен​​, чтобы бросить это (checked) исключение, оно должно "reinterrupt себя" со следующим заклинанием:
Thread.currentThread().interrupt();
Это гарантирует, что Thread будет reraise InterruptedException, как он сумеет.

А что если поток не реагирует на Thread.interrupt?


В некоторых случаях вы можете использовать специфические трюки. Например, если поток ожидает сокет, вы можете закрыть сокет, чтобы заставить поток немедленно вернуться. К сожалению, действительно нет никакой общеприменимой техники. Следует отметить, что во всех ситуациях, когда ожидающий поток не реагирует на Thread.interrupt, он не будет реагировать и на Thread.stop. Такие случаи включают преднамеренную атаку отказ-в-обслуживании и операции ввода/вывода, для которых thread.stop и thread.interrupt не работают должным образом.

Почему не рекомендуются Thread.suspend и Thread.resume?


Thread.suspend по своей природе подвержен deadlock. Если целевой поток удерживает блокировку на мониторе защиты критических системных ресурсов, когда он приостановлен, ни один поток не может получить доступ к этому ресурсу до возобновления целевого потока. Если поток, который возобновит целевой поток пытается заблокировать этот монитор перед вызовом resume, то будет deadlock. Такие deadlock ситуации обычно проявляются как «замороженные» процессы.

Что я должен использовать вместо Thread.suspend и Thread.resume?


Как и с Thread.stop, благоразумный подход заключается в том, что "целевой поток" опрашивает переменную где указывается желаемое состояние потока (активен или приостановлен). Когда желаемое состояние «приостановлено», поток ожидает использованием Object.wait. Когда поток возобновляется, целевой поток получает уведомление использованием Object.notify.

Например, предположим, апплет содержит следующий обработчик события mousePressed, который переключает состояние потока с именем blinker:
private boolean threadSuspended;

    Public void mousePressed(MouseEvent e) {
        e.consume();

        if (threadSuspended)
            blinker.resume();
        else
            blinker.suspend();  // DEADLOCK-PRONE!

        threadSuspended = !threadSuspended;
    }
Вы можете избежать использования Thread.suspend и Thread.resume заменив обработчик события выше:
public synchronized void mousePressed(MouseEvent e) {
        e.consume();

        threadSuspended = !threadSuspended;

        if (!threadSuspended)
            notify();
    }
и добавить следующий код в "цикл run":
synchronized(this) {
                    while (threadSuspended)
                        wait();
                }
Метод wait бросает InterruptedException, поэтому он должен быть внутри try ... catch. Это прекрасно, можно положить его в один блок со sleep. Проверка должна следовать (а не предшествовать) sleep, так что окна немедленно перерисовываются, когда поток «возобновлён». В результате метод run выглядит так:
public void run() {
        while (true) {
            try {
                Thread.currentThread().sleep(interval);

                synchronized(this) {
                    while (threadSuspended)
                        wait();
                }
            } catch (InterruptedException e){
            }
            repaint();
        }
    }
Обратите внимание, что notify в методе mousePressed и wait в методе run внутри синхронизированного блока. Это требует язык и гарантирует, что wait и notify правильно сериализуются. С практической точки зрения, это позволит избежать race conditions, которые могут привести к «приостановке» потока, пропуска уведомления и таким образом приостановлению на неопределенный срок.

В то время как цена синхронизации в Java уменьшается по мере созревания платформы, она никогда не будет бесплатной. Простой трюк может быть использован для удаления синхронизации, которую мы добавили к каждой итерации "цикла run". Синхронизированный блок, который был добавлен заменяется несколько более сложным куском кода, который входит в синхронизированный блок, только если поток фактически была приостановлен​​:
if (threadSuspended) {
                    synchronized(this) {
                        while (threadSuspended)
                            wait();
                    }
                }
При отсутствии явной синхронизации threadSuspended должен быть volatile, чтобы обеспечить быструю связь suspend-запроса.

В результате метод run:
private boolean volatile threadSuspended;

    public void run() {
        while (true) {
            try {
                Thread.currentThread().sleep(interval);

                if (threadSuspended) {
                    synchronized(this) {
                        while (threadSuspended)
                            wait();
                    }
                }
            } catch (InterruptedException e){
            }
            repaint();
        }
    }

Могу ли я комбинировать две техники для создания потока, который может быть безопасно «остановлен» или «приостановлен»?


Да, это достаточно просто. Одна тонкость в том, что целевой поток может быть приостановлен уже в то время, другой поток пытается остановить его. Если метод stop просто устанавливает состояние переменной (blinker) на null, целевой поток останется приостановленым (ожидающий на мониторе), а не выйдет изящно, как положено. Если апплет перезапустить, несколько потоков может в конечном итоге ожидать на мониторе в то же время, в результате непредсказуемое поведение.

Чтобы исправить эту ситуацию, метод stop должен гарантировать, что целевой поток возобновляется сразу же, если он приостановлен. После целевой поток возобновляется, он должен немедленно осознать, что он было остановлен, и красиво выйти. Вот как в результате выглядят методы run и stop:
public void run() {
        Thread thisThread = Thread.currentThread();
        while (blinker == thisThread) {
            try {
                thisThread.sleep(interval);

                synchronized(this) {
                    while (threadSuspended && blinker==thisThread)
                        wait();
                }
            } catch (InterruptedException e){
            }
            repaint();
        }
    }

    public synchronized void stop() {
        blinker = null;
        notify();
    }
Если метод stop вызывает Thread.interrupt, как описано выше, то не нужно вызывать notify, а также, но все же должны быть синхронизированы. Это гарантирует, что целевой поток не пропустит своё прерывание из-за race condition.

А как насчёт Thread.destroy?


Thread.destroy никогда не был реализован. Если бы это было реализовано, это будет подвержено deadlock как и Thread.suspend. (На самом деле, это примерно эквивалентно Thread.suspend без возможности последующего Thread.resume). Мы не реализуем его в это время, но мы не deprecating его (предотвращение его реализации в будущем). Хотя, конечно, он будет склонен к deadlock, как было заявлено, но могут быть обстоятельства, когда программа готова на deadlock, а не прямой выход.

Почему Runtime.runFinalizersOnExit не рекомендуется?


Потому что это по своей сути небезопасно. Это может привести к финализации живых объектов, в то время как другие потоки одновременно манипулируют этими объектами, в итоге странности или deadlock. Хотя эта проблема могла бы быть предотвращена, если класс, объекты которого в настоящее время финализируются были написан так, чтобы "defend against" этого вызова, большинство программистов не защищаются против такого. Они предполагают, что объект мертв на момент вызова финализатора.

Кроме того, вызов не «потокобезопаснен» в том смысле, что она устанавливает VM-глобальный флаг. Это заставляет каждый класс иметь finalizer, чтобы защититься от финализации живых объектов!

Комментариев нет:

Отправить комментарий