Почему использование volatile с синхронизированным блоком?

Я видел несколько примеров в java, где они выполняли синхронизацию в блоке кода, чтобы изменить некоторую переменную, в то время как эта переменная была объявлена ​​летучей исходной. Я видел это в примере singleton-класса, где они объявили уникальный экземпляр как изменчивый, и они sychronized блок, который инициализирует этот экземпляр... Мой вопрос заключается в том, почему мы объявляем его изменчивым, пока мы его синхронизируем, почему нам нужно делать оба? не является одним из них, достаточным для другого?

public class someClass {
volatile static uniqueInstance = null;

public static someClass getInstance() {
        if(uniqueInstance == null) {
            synchronized(someClass.class) {
                if(uniqueInstance == null) {
                    uniqueInstance = new someClass();
                }
            }
        }
        return uniqueInstance;
    }

заблаговременно.

Ответ 1

Синхронизация сама по себе будет достаточной в этом случае, если первая проверка была в синхронизированном блоке (но это не так, и один поток может не увидеть изменения, выполненные другим, если переменная не была изменчивой). Недостаточно одного энергопотребления, потому что вам нужно выполнить более одной операции атомарно. Но будьте осторожны! Здесь у вас есть так называемая двойная проверка - общая идиома, которая, к сожалению, не работает надежно. Я думаю, что это изменилось с Java 1.6, но этот код может быть рискованным.

EDIT: когда переменная изменчива, этот код работает правильно с JDK 5 (не 6, как я писал ранее), но он не будет работать так, как ожидалось, в JDK 1.4 или ранее.

Ответ 2

Это использует двойную проверку блокировки, обратите внимание, что if(uniqueInstance == null) не находится в пределах синхронизированной части.

Если uniqueInstance не является изменчивым, он может быть "инициализирован" частично сконструированным объектом, где его части не видны, кроме потока, выполняемого в блоке synchronized. volatile делает это все или ничего в этом случае.

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

if(uniqueInstance == null) {
      uniqueInstance = new someClass(); <---- here

И вы создаете 2 объекта SomeClass, которые побеждают цель.

Строго говоря, вам не нужна летучесть, метод мог быть

public static someClass getInstance() {
    synchronized(FullDictionary.class) {
         if(uniqueInstance == null) {
             uniqueInstance = new someClass();
          }
         return uniqueInstance;
    }
}

Но это приводит к синхронизации и сериализации каждого потока, который выполняет getInstance().

Ответ 3

Этот пост объясняет идею неустойчивости.

Он также рассматривается в основной работе, Java Concurrency на практике.

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

Ответ 4

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

Ответ 5

Мои два цента здесь

Фрист - быстрое объяснение интуиции этого кода

if(uniqueInstance == null) {
        synchronized(someClass.class) {
            if(uniqueInstance == null) {
                uniqueInstance = new someClass();
            }
        }
    }

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

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

Последний изменчивый модификатор гарантирует, что все потоки будут видеть одну и ту же копию, поэтому первая проверка за пределами синхронизированного блока увидит значение uniqueInstance таким образом, что это "синхронизировано", с синхронизированным блоком. Без изменчивого модификатора один поток может назначить значение uniqueInstance, но другой поток может не увидеть его с помощью первой проверки. (Хотя вторая проверка увидит его)