Подтвердить что ты не робот

Java: ссылка escape

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

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

Ответ 1

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

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

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

  • Согласно JLS 17.5, окончательные атрибуты объекта можно безопасно использовать без синхронизации. Однако это справедливо только в том случае, если ссылка на объект не опубликована (не исчезает) до завершения его конструктора. Если вы нарушите это правило, результатом будет коварная ошибка Concurrency, которая может укусить вас, когда код будет выполнен на многоядерных/многопроцессорных машинах.

Пример 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!" вместо "ОК", указывая, что основной поток заметил объект Unsafe в "невозможном" состоянии из-за небезопасной публикации через массив leak. Это произойдет или не будет зависеть от вашей JVM и вашей аппаратной платформы.

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

Ответ 2

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

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

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

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

Ответ 3

Я предполагаю, что метод doSomething объявлен в классе ThisEscape, и в этом случае ссылка, безусловно, может "сбежать".
I.e., некоторое событие может инициировать этот 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;
    }
}

Ответ 4

У меня был точно такой же вопрос, когда я читал " Java Concurrency In Practice" Брайана Гетца.

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

issue192 (где он расширяет пример и обеспечивает условие гонки).

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