Java: сделать все поля окончательными или изменчивыми?

Если у меня есть объект, который разделяется между потоками, мне кажется, что каждое поле должно быть либо final либо volatile со следующими соображениями:

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

  • если поле никогда не изменится, сделайте его final.

Однако я не смог ничего найти по этому поводу, поэтому мне интересно, является ли эта логика ошибочной или просто слишком очевидной?

EDIT, конечно, вместо volatile можно использовать final AtomicReference или аналогичный.

РЕДАКТИРОВАТЬ, например, см. Является ли метод getter альтернативой volatile в Java?

РЕДАКТИРОВАТЬ, чтобы избежать путаницы: Этот вопрос о недействительности кэша! Если два потока работают с одним и тем же объектом, поля объектов можно кэшировать (для каждого потока), если они не объявлены как энергозависимые. Как я могу гарантировать, что кеш недействителен правильно?

ЗАКЛЮЧИТЕЛЬНОЕ РЕДАКТИРОВАНИЕ Спасибо @Peter Lawrey, который указал мне на JLS §17 (модель памяти Java). Насколько я вижу, это говорит о том, что синхронизация устанавливает отношение между операциями, которое происходит до того, что поток увидит обновления из другого потока, если эти обновления произошли до того, например, если получатель и установщик для энергонезависимого поля synchronized.

Ответ 1

Хотя я считаю, что private final вероятно, должен был использоваться по умолчанию для полей и переменных с ключевым словом типа var что делает его изменчивым, использование volatile, когда оно вам не нужно,

  • намного медленнее, часто примерно в 10 раз медленнее.
  • обычно не обеспечивает необходимую вам безопасность потоков, но может затруднить поиск таких ошибок, уменьшая вероятность их появления.
  • В отличие от final который улучшает ясность, говоря, что это не должно быть изменено, использование volatile когда оно не нужно, может сбить с толку, так как читатель пытается понять, почему он был изменяемым.

если поле должно быть изменено (указать на другой объект, обновить значение примитива), то поле должно быть изменчивым, чтобы все другие потоки работали с новым значением.

Хотя это хорошо для чтения, рассмотрим этот тривиальный случай.

volatile int x;

x++;

Это не потокобезопасно. Как это так же, как

int x2 = x;
x2 = x2 + 1; // multiple threads could be executing on the same value at this point.
x = x2;

Хуже всего то, что использование volatile затруднит поиск такого рода ошибок.

Как указывает yshavit, обновление нескольких полей сложнее обойти с помощью volatile например, HashMap.put(a, b) обновляет несколько ссылок.

Синхронизация методов, обращающихся к указанному полю, недостаточна, поскольку они могут возвращать кэшированное значение.

Синхронизированный дает вам всю память гарантирует volatile и многое другое, поэтому он значительно медленнее.

ПРИМЕЧАНИЕ. Просто synchronized -ing не всегда достаточно для каждого метода. StringBuffer синхронизирует каждый метод, но он является худшим, чем бесполезным в многопоточном контексте, так как его использование может быть подвержено ошибкам.

Слишком легко предположить, что достижение безопасности нитей похоже на разбрызгивание волшебной пыли, добавьте немного волшебной безопасности ниток, и ваши ошибки исчезнут. Проблема в том, что безопасность ниток больше похожа на ведро с множеством отверстий. Заткните самые большие отверстия, и ошибки могут исчезнуть, но если вы не закроете их все, у вас не будет безопасности для нитей, но найти ее будет сложнее.

С точки зрения синхронизированного против изменчивого, это заявляет

Другие механизмы, такие как чтение и запись изменчивых переменных и использование классов в пакете java.util.concurrent, предоставляют альтернативные способы синхронизации.

https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html

Ответ 2

Создание полей вам не нужно, чтобы изменить final является хорошей идеей, независимо от проблем многопоточности. Это облегчает рассуждение экземпляров класса, потому что вам легче узнать, в каком состоянии он находится.

С точки зрения придания volatile другим областям:

Синхронизация методов, обращающихся к указанному полю, недостаточна, поскольку они могут возвращать кэшированное значение.

Вы увидите только кэшированное значение, если получите доступ к значению вне синхронизированного блока.

Все обращения должны быть правильно синхронизированы. Конец одного синхронизированного блока гарантированно наступит до начала другого синхронизированного блока (при синхронизации на том же мониторе).

Есть по крайней мере пара случаев, когда вам все еще нужно использовать синхронизацию:

  • Вы хотели бы использовать синхронизацию, если вам нужно было прочитать, а затем обновить одно или несколько полей атомарно.
    • Вы можете избежать синхронизации для определенных обновлений одного поля, например, если вы можете использовать класс Atomic* вместо "простого старого поля"; но даже для обновления одного поля вам все равно может потребоваться монопольный доступ (например, добавление одного элемента в список при удалении другого).
  • Кроме того, volatile/final может быть недостаточно для не поточных безопасных значений, таких как ArrayList или массив.

Ответ 3

Если объект совместно используется потоками, у вас есть два варианта:

1. Сделать этот объект только для чтения

Таким образом, обновления (или кэш) не имеют никакого влияния.

2. Синхронизировать на самом объекте

Аннулирование кэша сложно. Очень сложно. Поэтому, если вам нужно гарантировать отсутствие устаревших значений, вы должны защитить это значение и защитить блокировку указанного значения.

Сделайте блокировку и значения закрытыми для разделяемого объекта, чтобы операции здесь были деталями реализации.

Чтобы избежать мертвых блокировок, эти операции должны быть как "атомарными", так и во избежание взаимодействия с другими любыми блокировками.