Эффективно неизменяемый объект

Я хочу убедиться, что я правильно понимаю поведение "Эффективно неизменяемых объектов" в соответствии с моделью памяти Java.

Скажем, у нас есть изменяемый класс, который мы хотим опубликовать как эффективно неизменяемый:

class Outworld {
  // This MAY be accessed by multiple threads
  public static volatile MutableLong published;
}

// This class is mutable
class MutableLong {
  private long value;

  public MutableLong(long value) {
    this.value = value;
  }

  public void increment() {
    value++;
  }

  public long get() {
    return value;
  }
}

Мы делаем следующее:

// Create a mutable object and modify it
MutableLong val = new MutableLong(1);
val.increment();
val.increment();
// No more modifications
// UPDATED: Let say for this example we are completely sure
//          that no one will ever call increment() since now

// Publish it safely and consider Effectively Immutable
Outworld.published = val;

Вопрос: Модель Java Memory Model гарантирует, что все потоки ДОЛЖНЫ иметь Outworld.published.get() == 3?

Согласно Java Concurrency In Practice, это должно быть правдой, но, пожалуйста, исправьте меня, если я неправильно.

3.5.3. Безопасные публикации Идиомы

Чтобы безопасно опубликовать объект, как ссылка на объект, так и состояние объекта должно быть видимым для других потоков одновременно. Правильно построенный объект можно безопасно опубликовать:
- Инициализация ссылки на объект из статического инициализатора; - Сохранение ссылки на него в поле volatile или AtomicReference; - Сохранение ссылки на него в конечное поле правильно построенного объекта; или
- Сохранение ссылки на него в поле, которое должным образом защищено блокировкой.

3.5.4. Эффективно неизменяемые объекты

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

Ответ 1

Да. Операции записи на MutableLong сопровождаются отношением happens-before (от изменчивого) до чтения.

(Возможно, что поток читает Outworld.published и передает его другому потоку. Теоретически это может видеть предыдущее состояние. На практике я этого не вижу.)

Ответ 2

Существует несколько условий, которые должны быть соблюдены для модели памяти Java, чтобы гарантировать, что Outworld.published.get() == 3:

  • фрагмент кода, который вы разместили, который создает и увеличивает значение MutableLong, затем устанавливает поле Outworld.published, должен происходить с видимостью между этапами. Один из способов добиться этого тривиально - иметь весь этот код, работающий в одном потоке, гарантируя "как-бы-серийную семантику". Я предполагаю, что вы намеревались, но считали, что это стоит указать.
  • чтение Outworld.published должно происходить - после семантики из назначения. Примером этого может быть тот же самый поток выполнить Outworld.published = val;, затем запустить другие потоки, которые могли бы прочитать значение. Это гарантировало бы "как бы последовательную" семантику, предотвращая повторный порядок чтения до назначения.

Если вы можете предоставить эти гарантии, JMM гарантирует, что все потоки будут видеть Outworld.published.get() == 3.


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

Для гарантии того, что ни одна другая нить не увидит другого значения для Outworld.published.get(), вы (разработчик) должны гарантировать, что ваша программа каким-либо образом не изменит значение. Либо путем последующего выполнения Outworld.published = differentVal;, либо Outworld.published.increment();. Хотя это можно гарантировать, это может быть намного проще, если вы создадите свой код, чтобы избежать как изменяемого объекта, так и использования статического нефинального поля в качестве глобальной точки доступа для нескольких потоков:

  • вместо публикации MutableLong скопируйте соответствующие значения в новый экземпляр другого класса, состояние которого не может быть изменено. Например: введите класс ImmutableLong, который присваивает value поле final при построении и не имеет метода increment().
  • вместо нескольких потоков, обращающихся к статическому нефинальному полю, передайте объект в качестве параметра в свои реализации Callable/Runnable. Это предотвратит возможность того, что один поток изгоев переназначает значение и мешает другим, и легче рассуждать, чем статическое полевое переназначение. (По общему признанию, если вы имеете дело с устаревшим кодом, это проще сказать, чем сделать).

Ответ 3

Возникает вопрос: гарантирует ли Java-модель памяти все потоки ДОЛЖЕН иметь Outworld.published.get() == 3?

Короткий ответ no. Поскольку другие потоки могут получить доступ к Outworld.published до того, как они будут прочитаны.

После момента выполнения Outworld.published = val; при условии, что никакие другие изменения, сделанные с помощью val - yes, всегда будут 3.

Но если какой-либо поток выполняет val.increment, тогда его значение может отличаться для других потоков.