Спецификация Java 11: 17.5. Чтение, изменение final полей

17.5.1. Семантика final полей

Пусть o будет объектом, а c будет конструктором для o, в котором записано final поле f. Замораживание (freeze action) последнего поля f из o происходит, когда c выходит, как обычно, так и внезапно.

Обратите внимание, что если один конструктор вызывает другой конструктор, а вызываемый конструктор устанавливает final поле, замораживание final поля происходит в конце вызываемого конструктора.

Для каждого выполнения на поведение чтения влияют два дополнительных частичных порядка: цепочка разыменования dereferences() и цепочка памяти (memory chain) mc(), которые считаются частью выполнения (и, таким образом, фиксируются для любого конкретного выполнения). Эти частичные порядки должны удовлетворять следующим ограничениям (которые не обязательно должны иметь уникальное решение):

  • Цепочка разыменования: если действие a является чтением или записью поля или элемента объекта o потоком t, который не инициализировал o, то должно существовать некоторое чтение r потоком t, которое видит адрес o, такое что r dereferences(r, a).
  • Цепочка памяти: существует несколько ограничений на порядок цепочки памяти:
    • Если r - это чтение, которое видит запись w, тогда это должно быть так, что mc(w, r).
    • Если r и a - действия, такие, что dereferences(r, a), то должно быть так, что mc(r, a).
    • Если w - это запись адреса объекта o потоком t, который не инициализировал o, тогда должно существовать некоторое чтение r потоком t, которое видит адрес o, такое, что mc(r, w).

Даны запись w, замораживание (freeze) f, действие a (которое не является чтением последнего поля), чтение r1 последнего поля, замороженного с помощью f, и чтение r2 такое, что hb(w, f), hb(f, a), mc(a, r1) и dereferences(r1, r2), то при определении, какие значения могут быть видны r2, мы рассматриваем hb(w, r2). (Это происходит-до (happens-before) того, как упорядочивание не закрывается транзитивно с другими случаями происходит-до (happens-before) упорядочения.)

Обратите внимание, что порядок разыменования является рефлексивным, и r1 может быть таким же, как r2.

Что касается чтения полей final, единственные записи, которые считаются выполненными до чтения final поля, - это записи, полученные с помощью семантики поля final.

17.5.2. Чтение final полей во время создания

Чтение final поля объекта в потоке, который конструирует этот объект, упорядочивается относительно инициализации этого поля в конструкторе по обычным правилам происходит-до (happens-before). Если чтение происходит после того, как поле установлено в конструкторе, он видит значение, присвоенное final полю, в противном случае - значение по умолчанию.

17.5.3. Последующее изменение final полей

В некоторых случаях, например при десериализации, системе потребуется изменить final поля объекта после построения. final поля могут быть изменены с помощью отражения и других средств, зависящих от реализации. Единственный шаблон, в котором это имеет разумную семантику, - это шаблон, в котором создается объект, а затем обновляются final поля объекта. Объект не должен быть видимым для других потоков, а также не должны читаться поля final, пока не будут завершены все обновления final полей объекта. Замораживание (freeze) final поля происходит как в конце конструктора, в котором установлено final поле, так и сразу после каждой модификации final поля посредством отражения или другого специального механизма.

Даже в этом случае возникает ряд сложностей. Если последнее поле инициализируется константным выражением (§15.28) в объявлении поля, изменения final поля могут не наблюдаться, поскольку использование этого final поля заменяется во время компиляции значением константного выражения.

Другая проблема заключается в том, что спецификация допускает агрессивную оптимизацию final полей. Внутри потока допустимо переупорядочивать чтения final поля с теми изменениями final поля, которые не выполняются в конструкторе.

Пример - агрессивная оптимизация final полей

class A {
    final int x;
    A() { 
        x = 1; 
    } 

    int f() { 
        return d(this,this); 
    } 

    int d(A a1, A a2) { 
        int i = a1.x; 
        g(a1); 
        int j = a2.x; 
        return j - i; 
    }

    static void g(A a) { 
        // использует отражение, чтобы изменить a.x на 2
    } 
}

В методе d компилятор может свободно переупорядочивать чтение x и вызов g. Таким образом, new A().f() может вернуть -1, 0 или 1.

Реализация может предоставить способ выполнения блока кода в контексте безопасности final поля (final-field-safe). Если объект создается в контексте, безопасном для final поля, чтения последнего поля этого объекта не будут переупорядочены с изменениями этого final поля, которые происходят в этом контексте, безопасном для final поля.

Контекст final-field-safe имеет дополнительную защиту. Если поток обнаружил неправильно опубликованную ссылку на объект, которая позволяет потоку видеть значение по умолчанию final поля, а затем, в контексте, безопасном для final поля, читает правильно опубликованную ссылку на объект, это будет гарантированно увидеть правильное значение final поля. В формализме код, выполняемый в контексте, безопасном для final поля, рассматривается как отдельный поток (только для целей семантики final поля).

В реализации компилятор не должен перемещать доступ к final полю в контекст, безопасный для final поля, или из него (хотя его можно перемещать вокруг выполнения такого контекста, пока объект не создается внутри этого контекста).

Одно место, где было бы уместно использование контекста final-field-safe, - это исполнитель или пул потоков. Выполняя каждый Runnable в отдельном final-field-safe контексте, исполнитель может гарантировать, что некорректный доступ одного Runnable к объекту o не приведет к удалению гарантий final полей для других Runnable, обрабатываемых тем же исполнителем.

17.5.4. Поля, защищенные от записи

Обычно final и статическое поле не может быть изменено. Однако System.in, System.out и System.err являются статическими final полями, которые по устаревшим причинам должны быть изменены с помощью методов System.setIn, System.setOut и System.setErr. Мы называем эти поля защищенными от записи, чтобы отличать их от обычных final полей.

Компилятору необходимо обрабатывать эти поля иначе, чем другие final поля. Например, чтение обычного final поля "невосприимчиво" к синхронизации: барьер, связанный с блокировкой или volatile чтением, не должен влиять на то, какое значение считывается из final поля. Поскольку значение защищенных от записи полей может изменяться, события синхронизации должны оказывать на них влияние. Следовательно, семантика диктует, что эти поля должны рассматриваться как обычные поля, которые не могут быть изменены пользовательским кодом, если только этот пользовательский код не находится в классе System.


Читайте также:


Комментарии

Популярные сообщения из этого блога

Методы класса Object в Java

Как получить текущий timestamp в Java

Основные опции JVM для повышения производительности и отладки