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, който обвивате с нишка.   -  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 обекти следователно се отказва от заключването на самия обект 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 добре като се замисля, коментарът на Peter Lawrey също има смисъл... който имплементира Runnable, който обвивате с Thread - person Suraj Chandran; 30.08.2011
comment
Ще го разгледам, благодаря. разширяване на нишката прилагане на изпълняваща се java е доста годна за търсене в Google. - person jlh; 30.08.2011
comment
Тук имам едно съмнение. В момента, в който notify се извика от метода за изключване, заключването ще бъде освободено, така че няма въпрос за запазване на заключването при присъединяване, тъй като това ще се изпълни само когато отново получи заключването. Правилно ли съм или пропускам нещо? - person AKS; 24.08.2013

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

Ето описание стъпка по стъпка на това, което се случва в този пример:

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

След това новата нишка може да влезе в метода на изчакване и да освободи заключването си. В този момент новата нишка преминава в латентно състояние, тя няма да се опита да получи ключалката отново, докато не бъде събудена. Нишката все още не се връща от метода на изчакване.

В този момент основната нишка се събужда от спящ режим и извиква изключване на обекта MyThread. Няма проблем с придобиването на заключването, защото новата нишка го освободи, след като започна да чака. Извикванията на основната нишка уведомяват сега. Влизайки в метода за присъединяване, основната нишка проверява дали новата нишка е все още жива, след което изчаква, освобождавайки заключването.

Уведомлението се случва, след като основната нишка освободи заключването. Тъй като новата нишка е била в режим на изчакване за заключване по времето, когато главната нишка е наречена notify, новата нишка получава известието и се събужда. Той може да получи заключването, да напусне метода на изчакване и да завърши изпълнението на метода run, като накрая освободи заключването.

Прекратяването на новата нишка кара всички нишки, чакащи нейното заключване, да получат известие. Това събужда основната нишка, тя може да получи заключването и да провери дали новата нишка е мъртва, след което ще излезе от метода за присъединяване и ще завърши изпълнението.

/**
 * 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 спецификацията на join(long) казва: Тази реализация използва цикъл от this.wait извиквания, обусловени от this.isAlive. Тъй като нишката прекратява, се извиква методът this.notifyAll.. Тъй като е включено изчакване, включва се и заключване. Отговорът на Пауло трябва да е бил базиран на Java 6 API, тъй като Java 7 току-що беше пусната по това време. - person Dheeru Mundluru; 07.12.2020