Java wait () / join (): почему это не тупиковая ситуация?

Учитывая следующий код Java:

public class Test {

    static private class MyThread extends Thread {
        private boolean mustShutdown = false;

        @Override
        public synchronized void run() {
            // loop and do nothing, just wait until we must shut down
            while (!mustShutdown) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    System.out.println("Exception on wait()");
                }
            }
        }

        public synchronized void shutdown() throws InterruptedException {
            // set flag for termination, notify the thread and wait for it to die
            mustShutdown = true;
            notify();
            join(); // lock still being held here, due to 'synchronized'
        }
    }

    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();

        try {
            Thread.sleep(1000);
            mt.shutdown();
        } catch (InterruptedException e) {
            System.out.println("Exception in main()");
        }
    }
}

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

Я рассуждаю следующим образом: вновь созданный MyThread будет выполнять run (), который объявлен как «синхронизированный», так что он может вызывать wait () и безопасно читать «mustShutdown»; во время этого вызова wait () блокировка снимается и повторно устанавливается при возврате, как описано в документации на wait (). Через одну секунду основной поток выполняет shutdown (), который снова синхронизируется, чтобы не обращаться к mustShutdown одновременно с его чтением другим потоком. Затем он пробуждает другой поток через notify () и ждет его завершения через join ().

Но, на мой взгляд, другой поток не может когда-либо вернуться из wait (), поскольку ему необходимо повторно получить блокировку объекта потока перед возвратом. Это невозможно, потому что shutdown () по-прежнему удерживает блокировку внутри join (). Почему он все еще работает и выходит правильно?


person jlh    schedule 30.08.2011    source источник
comment
Это из-за подобных побочных эффектов, которые напрямую расширяют Thread, не одобряются. Вы должны реализовать Runnable, который вы оборачиваете с помощью Thread.   -  person Peter Lawrey    schedule 30.08.2011


Ответы (3)


Метод join () внутренне вызывает wait (), что приводит к снятию блокировки (объекта Thread).

Смотрите код join () ниже:

public final synchronized void join(long millis) 
    throws InterruptedException {
    ....
    if (millis == 0) {
       while (isAlive()) {
         wait(0);  //ends up releasing lock
       }
    }
    ....
}

Причина, по которой ваш код видит это и не видит в целом:. Причина, по которой ваш код видит это, а не наблюдается в целом, заключается в том, что метод join () waits () в Thread object и, следовательно, снимает блокировку с самого объекта Thread, и поскольку ваш метод run () также синхронизируется с тем же объектом Thread, вы видите этот неожиданный сценарий.

person Suraj Chandran    schedule 30.08.2011
comment
Это действительно объясняет. Но, как отмечает Паоло Эберманн, в документации об этом ничего не говорится. Могу ли я положиться на то, что join () снимет блокировку? - person jlh; 30.08.2011
comment
Абсолютно да. Код не изменился с тех пор, как я достигла половой зрелости. Не волнуйся. - person Suraj Chandran; 30.08.2011
comment
@jlh хорошо, если подумать, комментарий Питера Лоури тоже имеет смысл ... который реализует Runnable, который вы обертываете с помощью Thread - person Suraj Chandran; 30.08.2011
comment
Я посмотрю на это, спасибо. Расширение потока реализует runnable java вполне подойдет googlable. - person jlh; 30.08.2011
comment
У меня есть одно сомнение. В момент вызова уведомления из метода выключения блокировка будет снята, поэтому нет вопроса о блокировке сохранения соединения, поскольку она будет выполнена только тогда, когда она снова получит блокировку. Я исправил или что-то упустил? - person AKS; 24.08.2013

Реализация Thread.join использует ожидание, которое снимает блокировку, поэтому не мешает другому потоку получить блокировку.

Вот пошаговое описание того, что происходит в этом примере:

Запуск потока MyThread в основном методе приводит к тому, что новый поток выполняет метод запуска MyThread. Основной поток спит целую секунду, давая новому потоку достаточно времени для запуска и получения блокировки на объекте MyThread.

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

В этот момент основной поток выходит из спящего режима и вызывает завершение работы объекта MyThread. У него нет проблем с получением блокировки, потому что новый поток освободил ее, как только начал ждать. Теперь основной поток вызывает уведомление. Вводя метод соединения, основной поток проверяет, что новый поток все еще жив, а затем ожидает, снимая блокировку.

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

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

/**
 * Waits at most <code>millis</code> milliseconds for this thread to 
 * die. A timeout of <code>0</code> means to wait forever. 
 *
 * @param      millis   the time to wait in milliseconds.
 * @exception  InterruptedException if any thread has interrupted
 *             the current thread.  The <i>interrupted status</i> of the
 *             current thread is cleared when this exception is thrown.
 */
public final synchronized void join(long millis) 
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
    while (isAlive()) {
    wait(0);
    }
} else {
    while (isAlive()) {
    long delay = millis - now;
    if (delay <= 0) {
        break;
    }
    wait(delay);
    now = System.currentTimeMillis() - base;
    }
}
}
person Nathan Hughes    schedule 30.08.2011

В дополнение к другим ответам: я не вижу упоминания о join() снятии каких-либо блокировок в документации по API, поэтому такое поведение фактически зависит от реализации.

Узнай из этого:

  • не создавайте подклассы Thread, вместо этого используйте реализацию Runnable, переданную вашему объекту потока.
  • не синхронизировать / ждать / уведомлять об объектах, которые вам не "принадлежат", например где вы не знаете, кто еще может синхронизировать / ждать / уведомлять об этом.
person Paŭlo Ebermann    schedule 30.08.2011
comment
Реализация Runnable вместо создания подкласса Thread кажется мне более сложной в этой конкретной ситуации ... И только MyThread когда-либо использует «synchronized», wait (), join () и notify (). Основной класс никогда его не использует. (Хотя основной поток делает.) Так что я не уверен насчет вашего второго пункта. - person jlh; 30.08.2011
comment
Дело в том, что join внутри использует wait(), чего вы не ожидали. Т.е. монитор объекта MyThread используется для двух целей: для синхронизации вашего собственного цикла выполнения / завершения работы и для синхронизации управления внутренними потоками. - person Paŭlo Ebermann; 30.08.2011
comment
Начиная с Java 7, в спецификации API соединения (long) говорится: Эта реализация использует цикл вызовов this.wait, обусловленных this.isAlive. Когда поток завершает работу, вызывается метод this.notifyAll. Поскольку задействовано ожидание, задействуется и блокировка. Ответ Пауло должен был быть основан на API Java 6, поскольку Java 7 только что была выпущена в то время. - person Dheeru Mundluru; 07.12.2020