Пример, который вы опубликовали в своем вопросе, взят из "Практика Java-параллелизма" , автор Brian Goetz et al.Это в разделе 3.2 «Публикация и побег».Я не буду пытаться воспроизвести детали этого раздела здесь.(Пойдите, купите копию для своей книжной полки, или позаимствуйте копию от своих коллег!)
Проблема, иллюстрируемая примером кода, состоит в том, что конструктор позволяет ссылке на конструируемый объект, чтобы "убежать" передконструктор заканчивает создание объекта.Это проблема по двум причинам:
Если ссылка выходит за пределы, что-то может использовать объект до того, как его конструктор завершит инициализацию, и увидеть его в несогласованном (частично инициализированном) состоянии.Даже если объект завершается после завершения инициализации, объявление подкласса может привести к его нарушению.
Согласно JLS 17.5 , конечные атрибуты объекта могут безопасно использоваться без синхронизации.Однако это верно только в том случае, если ссылка на объект не публикуется (не удаляется) до завершения его конструктора.Если вы нарушите это правило, результатом будет коварная ошибка параллелизма, которая может укусить вас при выполнении кода на многоядерных / многопроцессорных компьютерах.
Пример ThisEscape
подлый, потому что ссылка экранируется через ссылку this
, неявно переданную анонимному конструктору класса EventListener
.Однако те же проблемы возникнут, если ссылка будет явно опубликована слишком рано.
Вот пример, иллюстрирующий проблему не полностью инициализированных объектов:
public class Thing {
public Thing (Leaker leaker) {
leaker.leak(this);
}
}
public class NamedThing extends Thing {
private String name;
public NamedThing (Leaker leaker, String name) {
super(leaker);
}
public String getName() {
return name;
}
}
Если вызов метода Leaker.leak(...)
getName()
для просочившегося объекта он получит null
... потому что в этот момент цепочка конструктора объекта еще не завершена.
Вот пример, иллюстрирующий проблему небезопасной публикации для final
Атрибуты.
public class Unsafe {
public final int foo = 42;
public Unsafe(Unsafe[] leak) {
leak[0] = this; // Unsafe publication
// Make the "window of vulnerability" large
for (long l = 0; l < /* very large */ ; l++) {
...
}
}
}
public class Main {
public static void main(String[] args) {
final Unsafe[] leak = new Unsafe[1];
new Thread(new Runnable() {
public void run() {
Thread.yield(); // (or sleep for a bit)
new Unsafe(leak);
}
}).start();
while (true) {
if (leak[0] != null) {
if (leak[0].foo == 42) {
System.err.println("OK");
} else {
System.err.println("OUCH!");
}
System.exit(0);
}
}
}
}
Некоторые запуски этого приложения может напечатать "OUCH!"вместо «OK», указывая, что основной поток обнаружил объект Unsafe
в «невозможном» состоянии из-за небезопасной публикации через массив leak
.Произойдет это или нет, будет зависеть от вашей JVM и вашей аппаратной платформы.
Теперь этот пример явно искусственный, но нетрудно представить, как такого рода вещи могут происходить в реальных многопоточных приложениях.
Текущая модель памяти Java была указана в Java 5 (3-е издание JLS) в результате JSR 133. До этого связанные с памятью аспекты Java были недооценены.указано.Источники, которые ссылаются на более ранние версии / издания, устарели, но информация о модели памяти в издании Гетца 1 актуальна.
Существуют некоторые технические аспекты модели памятикоторые, по-видимому, нуждаются в пересмотре;см. https://openjdk.java.net/jeps/188 и https://www.infoq.com/articles/The-OpenJDK9-Revised-Java-Memory-Model/. Однако эта работа еще не появилась в версии JLS.