Запрещает ли запущенный поток в объекте собирать мусор в Java? - PullRequest
9 голосов
/ 26 января 2009

С учетом кода:

 new Thread(new BackgroundWorker()).start();

Интуитивно понятно, что экземпляр BackgroundWorker должен быть в безопасности от GC до тех пор, пока поток не выйдет, но так ли это? И почему ?

Edit:

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

Отправленные ответы действительно превосходны. Я буду награждать Software Monkey зеленым флажком. Обратите внимание, что ответ Даррона одинаково действителен, но Software Monkey объяснила проблему, с которой столкнулся I ; это был ответ, который работал для меня.

Спасибо всем за то, что сделали это незабываемым делом;)

Ответы [ 6 ]

12 голосов
/ 26 января 2009

Да, потому что GC может собирать только объекты, недоступные для какого-либо потока, и Thread должен содержать ссылку на его работоспособность (иначе он не сможет вызвать его). Итак, ясно, что ваш объект Runnable доступен во время работы потока.

Независимо от семантики, необходимой для выполнения , ваш объект не будет GC'd, пока он больше не будет доступен для этого нового потока или любого другого; это будет по крайней мере достаточно долго, чтобы вызвать run () вашего Runnable, и на весь срок жизни потока, если этот поток сможет достичь экземпляра Runnable, поэтому ваша конструкция гарантированно безопасна по спецификации JVM.


РЕДАКТИРОВАТЬ: Потому что Даррон избивает это до смерти, и некоторые, кажется, убеждены его аргументом, я собираюсь расширить мое объяснение, основанное на его.

Предположим на данный момент, что никто, кроме самой Thread, не может вызывать Thread.run (),

В этом случае было бы правильно, чтобы реализация Thread.run () по умолчанию выглядела следующим образом:

void run() {
    Runnable tmp = this.myRunnable;  // Assume JIT make this a register variable.
    this.myRunnable = null;          // Release for GC.
    if(tmp != null) {
        tmp.run();         // If the code inside tmp.run() overwrites the register, GC can occur.
        }
    }

Я утверждаю, что в этом случае tmp все еще является ссылкой на исполняемый объект, достижимый потоком, выполняющимся в Thread.run (), и, следовательно, не подходит для GC.

Что если (по какой-то необъяснимой причине) код выглядит так:

void run() {
    Runnable tmp = this.myRunnable;  // Assume JIT make this a register variable.
    this.myRunnable = null;          // Release for GC.
    if(tmp != null) {
        tmp.run();         // If the code inside tmp.run() overwrites the register, GC can occur.
        System.out.println("Executed runnable: "+tmp.hashCode());
        }
    }

Очевидно, что экземпляр, на который ссылается tmp, не может быть GC'd, пока выполняется tmp.run ().

Я думаю, что Даррон ошибочно полагает, что достижимо означает только те ссылки, которые могут быть найдены путем поиска ссылок на экземпляры, начинающихся со всех экземпляров Thread как корней, а не определяемых как ссылка, которую можно увидеть любым исполняющим потоком. Либо так, либо я ошибаюсь, веря в обратное.

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

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

Кроме того, вся дискуссия спорна. Если единственная достижимая ссылка находится в методе Thread.run (), поскольку прогон runnable не ссылается на его экземпляр, и никакой другой ссылки на экземпляр не существует, включая неявную this , переданную методу run () (в байт-коде, а не в качестве объявленного аргумента), тогда не имеет значения, собран ли экземпляр объекта - это, по определению, не может причинить вреда, поскольку не требуется выполнять код, если неявный этот был оптимизирован . В таком случае, даже если Даррон и прав, конечный практический результат состоит в том, что конструкция, постулируемая ФП, совершенно безопасна. В любом случае. Это не важно Позвольте мне повторить это еще раз, просто для ясности - в конце анализа это не имеет значения .

5 голосов
/ 26 января 2009

Это безопасно. JVM удерживает ссылку на каждую нить. Поток держит экземпляр Runnable, переданный в его конструктор. Таким образом, Runnable является сильно достижимым и не будет собираться в течение жизни Thread.

Мы знаем, что поток содержит ссылку на исполняемый файл из-за javadoc для Thread.run () :

Если этот поток был создан с использованием отдельный объект Runnable, затем метод запуска этого объекта Runnable называется; в противном случае этот метод ничего и не возвращает.

5 голосов
/ 26 января 2009

Да, это безопасно. Причина не так очевидна, как вы думаете.

Тот факт, что код в BackgroundWorker выполняется, не делает его безопасным - рассматриваемый код может не ссылаться ни на какие члены текущего экземпляра, что позволяет оптимизировать «this».

Однако, если вы внимательно прочитаете спецификацию метода run () класса java.lang.Thread, вы увидите, что объект Thread должен сохранять ссылку на Runnable, чтобы выполнить свой контракт.

РЕДАКТИРОВАТЬ: потому что за этот ответ я был несколько раз отклонен, я собираюсь расширить мое объяснение.

Предположим на данный момент, что никто, кроме самой Thread, не может вызывать Thread.run (),

В этом случае реализация Thread.run () по умолчанию выглядит так:

void run() {
    Runnable tmp = this.myRunnable;  // Assume JIT make this a register variable.
    this.myRunnable = null;          // Release for GC.
    if (tmp != null)
        tmp.run();         // If the code inside tmp.run() overwrites the register, GC can occur.
}

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

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

1 голос
/ 26 января 2009

Я готов поспорить, что JVM включает ссылку на каждый объект потока, который активен или может быть запланирован в его корневом наборе, но у меня нет спецификации, чтобы подтвердить это.

1 голос
/ 26 января 2009

Да, потому что Thread хранит внутреннюю ссылку на Runnable (в конце концов, должен знать, что запускать ).

0 голосов
/ 26 января 2009

Нет, я не думаю, что это небезопасно.

На практике вам почти наверняка это сойдет с рук. Тем не менее, модель памяти Java удивляет. На самом деле, только на прошлой неделе в списке рассылки JMM обсуждалось, как планировать добавить метод «поддерживать объекты в живых». В настоящее время финализатор может быть запущен без отношения «до и после» при выполнении метода-члена. На данный момент вам технически необходимо представить реализацию до появления события путем синхронизации повсеместно или написания некоторой переменной в конце каждого метода и чтения этой переменной в финализаторе.

Как отмечает Даррон, если вы можете получить объект Thread (например, через Thread.enumerate), то вы можете вызвать run для него, что вызывает Runnable 'run. Однако я до сих пор не думаю, что происходит до .

Мой совет: не пытайтесь быть слишком «умным».

...