Java: ссылка на выход - PullRequest
       26

Java: ссылка на выход

23 голосов
/ 14 сентября 2010

Прочитайте, что следующий код является примером "небезопасной конструкции", поскольку он позволяет этой ссылке избежать. Я не мог понять, как «это» ускользает. Я довольно новичок в мире Java. Может ли кто-нибудь помочь мне понять это.

public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(
            new EventListener() {
                public void onEvent(Event e) {
                    doSomething(e);
                }
            });
    }
}

Ответы [ 4 ]

23 голосов
/ 14 сентября 2010

Пример, который вы опубликовали в своем вопросе, взят из "Практика Java-параллелизма" , автор Brian Goetz et al.Это в разделе 3.2 «Публикация и побег».Я не буду пытаться воспроизвести детали этого раздела здесь.(Пойдите, купите копию для своей книжной полки, или позаимствуйте копию от своих коллег!)

Проблема, иллюстрируемая примером кода, состоит в том, что конструктор позволяет ссылке на конструируемый объект, чтобы "убежать" передконструктор заканчивает создание объекта.Это проблема по двум причинам:

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

  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.

13 голосов
/ 14 сентября 2010

У меня было точно такое же сомнение.

Дело в том, что каждый класс, экземпляр которого создается внутри другого класса, имеет ссылку на включающий класс в переменной $this.

Это то, что java называет синтетическим , это не то, что вы определяете, чтобы быть там, а то, что java делает для вас автоматически.

Если вы хотите убедиться в этом самиустановите точку останова в строке doSomething(e) и проверьте, какие свойства имеет EventListener.

6 голосов
/ 14 сентября 2010

Я предполагаю, что метод doSomething объявлен в классе ThisEscape, и в этом случае ссылка, безусловно, может "сбежать".
Т.е. некоторое событие может вызвать это EventListener сразу после его создания и до завершения выполнения конструктора ThisEscape. А слушатель, в свою очередь, вызовет метод экземпляра ThisEscape.

Я немного изменю твой пример. Теперь переменная var может быть доступна в методе doSomething до того, как она назначена в конструкторе.

public class ThisEscape {
    private final int var;

    public ThisEscape(EventSource source) {
        source.registerListener(
            new EventListener() {
                public void onEvent(Event e) {
                    doSomething(e);
                }
            }
        );

        // more initialization
        // ...

        var = 10;
    }

    // result can be 0 or 10
    int doSomething(Event e) {
        return var;
    }
}
3 голосов
/ 26 ноября 2014

У меня точно такой же вопрос, когда я читал « Параллелизм Java на практике » Брайана Гетца.

Ответ Стивена С (принятый)отлично!Я только хотел добавить к этому еще один ресурс, который я обнаружил.Это из JavaSpecialists , где доктор Хайнц М. Кабуц анализирует именно тот пример кода, который опубликовал devnull .Он объясняет, какие классы генерируются (внешние, внутренние) после компиляции и как экранируется this.Я нашел это объяснение полезным, поэтому мне захотелось поделиться с ним :)

Issue192 (где он расширяет пример и предоставляет условие гонки).* (где он объясняет, какие классы генерируются после компиляции и как this убегает.)

...