Использование ключевого слова volatile с изменяемым объектом

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

В приведенном ниже примере работает ли он, если несколько потоков обращаются к volatile Mutable m и меняют value?

Пример

class Mutable {
    private int value;
    public int get()
    {
        return a;
    }
    public int set(int value)
    {
        this.value = value;
    }
}

class Test {
    public volatile Mutable m;
}

Ответ 1

Это своего рода пояснение по некоторым деталям волатильности. Написание этого здесь, потому что это слишком много для комментария. Я хочу привести несколько примеров, которые показывают, как изменчивость влияет на видимость и как это изменилось в jdk 1.5.

Учитывая следующий пример кода:

public class MyClass
{
  private int _n;
  private volatile int _volN;

  public void setN(int i) {
    _n = i;
  }
  public void setVolN(int i) {
    _volN = i;
  }
  public int getN() { 
    return _n; 
  }
  public int getVolN() { 
    return _volN; 
  }

  public static void main() {
    final MyClass mc = new MyClass();

    Thread t1 = new Thread() {
      public void run() {
        mc.setN(5);
        mc.setVolN(5);
      }
    };

    Thread t2 = new Thread() {
      public void run() {
        int volN = mc.getVolN();
        int n = mc.getN();
        System.out.println("Read: " + volN + ", " + n);
      }
    };

    t1.start();
    t2.start();
  }
}

Поведение этого тестового кода хорошо определено в jdk1.5 +, но не четко определено pre-jdk1.5.

В мире pre-jdk1.5 не было определённой взаимосвязи между изменчивыми доступами и нелетучими обращениями. поэтому выход этой программы может быть:

  • Читайте: 0, 0
  • Читать: 0, 5
  • Читать: 5, 0
  • Читать: 5, 5

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

  • Читайте: 0, 0
  • Читать: 0, 5
  • Чтение: 5, 0 < - невозможно
  • Читать: 5, 5

Выход 3. невозможен, потому что показание "5" из volatile _volN устанавливает точку синхронизации между двумя потоками, что означает, что все действия из t1, принятые до назначения на _volN, должны быть видимыми для t2.

Дальнейшее чтение:

Ответ 2

В вашем примере ключевое слово volatile гарантирует, что последняя ссылка, написанная любым потоком, на "m" будет видна для любого потока, читающего "m" впоследствии.

Это не гарантирует ничего о вашем get().

Итак, используйте следующую последовательность:

Thread-1: get()     returns 2
Thread-2: set(3)
Thread-1: get()    

для вас вполне законно возвращаться 2, а не 3. volatile ничего не меняет.

Но если вы измените свой класс Mutable на это:

class Mutable {
    private volatile int value;
    public int get()
    {
        return a;
    }
    public int set(int value)
    {
        this.value = value;
    }
}

Тогда гарантируется, что второй get() из Thread-1 вернет 3.

Обратите внимание, однако, что volatile обычно не лучший метод синхронизации.

В вашем простом примере get/set (я знаю это просто пример) класс, подобный AtomicInteger, используя правильную синхронизацию и фактически предоставляя полезные методы, будет лучше.

Ответ 3

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

В соответствии с Wikipedia у вас есть:

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

Итак, в основном у вас есть то, что, объявив поле volatile, взаимодействие с ним создает "точку синхронизации", после чего любые изменения будут видны в других потоках. Но после этого использование get() или set() несовместимо. Java Spec содержит более подробное объяснение.

Ответ 4

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

Как вы не сказали нам, что такое "правильное" поведение вашего кода, на вопрос нельзя ответить.

Ответ 5

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