Разница между синхронизацией чтения полей и изменчивым - PullRequest
10 голосов
/ 23 июня 2010

В хорошей статье с некоторыми советами по параллелизму пример был оптимизирован для следующих строк:

double getBalance() {
    Account acct = verify(name, password);
    synchronized(acct) { return acct.balance; }
}

Если я правильно понимаю, смысл синхронизации состоит в том, чтобы гарантировать, что значение acct.balance, читаемое этим потоком, является текущим и что любые ожидающие записи в поля объекта в acct.balance также записываются в основную память.

Этот пример заставил меня задуматься: не будет ли эффективнее просто объявить acct.balance (то есть поле баланса класса Account) как volatile? Он должен быть более эффективным, сэкономить вам все synchronize при доступе к acct.balance и не блокировать весь объект acct. Я что-то упустил?

Ответы [ 3 ]

13 голосов
/ 23 июня 2010

Вы правы.volatile обеспечивает гарантию видимости.Synchronized обеспечивает как гарантию видимости, так и сериализацию защищенных участков кода.Для ОЧЕНЬ простых ситуаций достаточно volatile, однако легко попасть в неприятности, используя volatile вместо синхронизации.

Если вы предполагаете, что Учетная запись имеет способ регулировки своего баланса, то volatile не достаточно хорош

public void add(double amount)
{
   balance = balance + amount;
}

Тогда у нас есть проблема, если баланс изменчив без какой-либо другой синхронизации.Если бы два потока попытались вызвать add () вместе, вы могли бы получить «пропущенное» обновление, где произошло бы следующее

Thread1 - Calls add(100)
Thread2 - Calls add(200)
Thread1 - Read balance (0)
Thread2 - Read balance (0)
Thread1 - Compute new balance (0+100=100)
Thread2 - Compute new balance (0+200=200)
Thread1 - Write balance = 100
Thread2 - Write balance = 200 (WRONG!)

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

Обычно я нахожу, что если при написании кода я думаю, "могу ли я использовать volatile вместо синхронизированного"Ответ вполне может быть «да», но время / усилие выяснить это наверняка и опасность ошибиться не стоит выгоды (незначительная производительность).

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

1 голос
/ 06 ноября 2016

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

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

Но для приведенного выше кода, volatile не гарантирует согласованность памяти, если несколько потоков изменяют баланс. AtomicReference с типом Double соответствует вашим целям.

Смежный вопрос SE:

Разница между энергозависимым и синхронизированным в Java

1 голос
/ 19 августа 2010

Объявление учетной записи как volatile подвержено следующим проблемам и ограничениям

1. "Поскольку другие потоки не могут видеть локальные переменные, объявление локальных переменных volatile бесполезно ."Более того, если вы попытаетесь объявить переменную в методе, в некоторых случаях вы получите ошибку компилятора.

double getBalance () {volatile Account acct = verify (name, password);// Неверно ..}

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

Если вам требуется синхронизация для координации изменений переменных из разных потоков, volatile не гарантирует вам атомарный доступ , потому что доступ к переменной volatile никогда не удерживает блокировку, он не подходит для случаев, когда мы хотим читать-обновлять-записывать как атомарную операцию.Если вы не уверены, что acct = verify (имя, пароль);является единственной атомарной операцией, вы не можете гарантировать исключительные результаты

Если переменная acct является ссылкой на объект, скорее всего, она может быть нулевой. Попытка синхронизации с нулевым объектом приведет кисключение NullPointerException с использованием синхронизации.(потому что вы эффективно синхронизируете ссылку, а не реальный объект) Где volatile не жалуется

Вместо этого вы можете объявить булеву переменную как volatile, как здесь

private volatile логическое someAccountflag;

public void getBalance () {Account acct;while (! someAccountflag) {acct = verify (имя, пароль);}}

Обратите внимание, что вы не можете объявить someAccountflag как синхронизированный, поскольку вы не можете синхронизировать примитив с синхронизированным, synchronized работает только с объектными переменными, где в качестве примитива или объектной переменнойможет быть объявлено как volatile

6. Конечные статические поля класса не должны быть volatile , JVM решает эту проблему.Таким образом, флаг someAccount не нужно даже объявлять как volatile, если он является окончательным статическим, или вы можете использовать ленивую одноэлементную инициализацию, делающую Account как объект singleton, и объявить его следующим образом: private final static AccountSingleton acc_singleton = new AccountSingleton ();

...