Почему volatile в Java не обновляет значение переменной?

Я читал, что volatile в Java позволяет разным потокам иметь доступ к одному и тому же полю и видеть изменения, внесенные другими потоками в это поле. Если это так, я бы предсказал, что когда первый и второй поток полностью запустятся, значение d будет увеличено до 4. Но вместо этого каждый поток увеличивает d до значения 2.

public class VolatileExample extends Thread {
private int countDown = 2;
private volatile int d = 0; 

    public VolatileExample(String name) {
        super(name);
        start();
    }

    public String toString() {
       return super.getName() + ": countDown " + countDown;
    }

public void run() {
        while(true) {
        d = d + 1;
    System.out.println(this + ". Value of d is " + d);
    if(--countDown == 0) return;
    }
}

public static void main(String[] args) {
    new VolatileExample("first thread");
    new VolatileExample("second thread");
    }
}

Результаты работы этой программы:

первый поток: countDown 2. Значение d равно 1

второй поток: countDown 2. Значение d равно 1

первый поток: countDown 1. Значение d равно 2

второй поток: countDown 1. Значение d равно 2

Я понимаю, что если я добавлю ключевое слово static в программу (то есть private static volatile int d = 0;), d будет увеличен до 4. И я знаю, что это потому, что d станет переменной, которую разделяет весь класс, а не каждый экземпляр получения копии.

Результаты выглядят так:

первый поток: countDown 2. Значение d равно 1

первый поток: countDown 1. Значение d равно 3

второй поток: countDown 2. Значение d равно 2

второй поток: countDown 1. Значение d равно 4

Мой вопрос в том, почему private volatile int d = 0; дают аналогичные результаты, если предполагается, что volatile разрешает совместное использование d между двумя потоками? То есть, если первый поток обновляет значение d до 1, то почему второй поток не получает значение d как 1, а не как ноль?


person herrington    schedule 16.02.2012    source источник
comment
Спасибо человеку (думаю, пост каким-то образом удалили), который предположил, что это проблема атомарности.   -  person herrington    schedule 16.02.2012
comment
Это был мой первый ответ, который я удалил, о котором вы говорите. Я удалил его, потому что, перечитав ваш вопрос, я понял, что проблема здесь немного сложнее, чем простая, которую вам нужно синхронизировать. Поэтому я написал новый ответ ниже, сосредоточившись на том, что я считал реальной проблемой (понимание экземпляра и статических полей, потоков и volatile), и просто кратко упомянул синхронизацию. :)   -  person Bruno Reis    schedule 16.02.2012


Ответы (3)


Здесь есть пара недоразумений. Вы, кажется, не совсем понимаете, что такое поток, что такое поле экземпляра и что такое статическое поле.

поле экземпляра — это область памяти, которая выделяется при создании экземпляра класса (т. е. область памяти выделяется для поля d при VolatileExample v = new VolatileExample()). Чтобы сослаться на это место в памяти из класса, вы делаете this.d (затем вы можете писать и читать из этого места в памяти). Чтобы сослаться на это место в памяти извне класса, оно должно быть доступным (т. е. не private), и тогда вы должны сделать v.d. Как видите, каждый экземпляр класса получает свою собственную ячейку памяти для своего собственного поля d. Итак, если у вас есть 2 разных экземпляра VolatileExample, у каждого будет свое собственное независимое поле d.

Статическое поле — это область памяти, которая выделяется после инициализации класса (что, если забыть о возможности использования нескольких ClassLoader, происходит ровно один раз). Итак, вы можете подумать, что статическое поле — это своего рода глобальная переменная. Чтобы сослаться на это место в памяти, вы должны использовать VolatileExample.d (доступность также применяется (т. е. если это private, это можно сделать только внутри класса)).

Наконец, поток выполнения — это последовательность шагов, которые будут выполняться JVM. Вы не должны думать о потоке как о классе или экземпляре класса Thread, это только запутает вас. Это так просто: последовательность шагов.

основная последовательность шагов определена в методе main(...). Это та последовательность шагов, которую JVM начнет выполнять при запуске вашей программы.

Если вы хотите запустить новый поток выполнения для одновременного запуска (т. е. вы хотите, чтобы отдельная последовательность шагов выполнялась одновременно), в Java вы делаете это, создавая экземпляр класса Thread и вызывая его метод start().

Давайте немного изменим ваш код, чтобы было легче понять, что происходит:

public class VolatileExample extends Thread {
  private int countDown = 2;
  private volatile int d = 0;

  public VolatileExample(String name) {
    super(name);
  }

  public String toString() {
    return super.getName() + ": countDown " + countDown;
  }

  public void run() {
    while(true) {
      d = d + 1;
      System.out.println(this + ". Value of d is " + d);
      if(--countDown == 0) return;
    }
  }

  public static void main(String[] args) {
    VolatileExample ve1 = new VolatileExample("first thread");
    ve1.start();
    VolatileExample ve2 = new VolatileExample("second thread");
    ve2.start();
  }
}

Строка VolatileExample ve1 = new VolatileExample("first thread"); создает экземпляр VolatileExample. Это выделит несколько ячеек памяти: 4 байта для countdown и 4 байта для d. Затем вы запускаете новый поток выполнения: ve1.start();. Этот поток выполнения будет получать доступ (чтение и запись) к ячейкам памяти, описанным ранее в этом абзаце.

Следующая строка, VolatileExample ve2 = new VolatileExample("second thread");, создает другой экземпляр VolatileExample, который выделяет 2 новых ячейки памяти: 4 байта для countdown ve2 и 4 байта для d ve2. Затем вы запускаете поток выполнения, который будет обращаться к ЭТИМ НОВЫМ ячейкам памяти, а не к тем, которые описаны в предыдущем абзаце.

Теперь, с volatile или без него, вы видите, что у вас есть два разных поля d: каждый поток работает с другим полем. Поэтому неразумно ожидать, что d будет увеличено до 4, так как не существует единственного d.

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

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

Однако это не гарантирует, что вы увидите значение 4, хранящееся в d. Это потому, что volatile решает проблему видимости, но не проблему атомарности: increment = read from main memory + operation on the value + write to main memory. Как видите, 2 разных потока могут прочитать начальное значение (0), оперировать (локально) с ним (получив 1), затем записать его в основную память (оба в конечном итоге запишут 1) — 2 приращения будут воспринимается только как 1.

Чтобы решить эту проблему, вы должны сделать приращение атомарной операцией. Для этого вам нужно либо использовать механизм синхронизации — мьютекс (synchronized (...) { ... } или явная блокировка) — либо класс, разработанный специально для этого: AtomicInteger.

person Bruno Reis    schedule 16.02.2012
comment
Спасибо за очень хорошо написанный ответ. Ваш ответ очень помог объяснить некоторые понятия, которые были мне непонятны. Итак, чтобы подвести итог, чтобы d последовательно увеличивалось до 4, вы бы поместили static и синхронизировали метод в run, используя что-то вроде synchronized (this)? Кроме того, когда я читал эту [статью] (javamex.com/tutorials/) , это означало, что два разных потока обращались к одной и той же переменной. Так что я запутался, что имел в виду автор. - person herrington; 16.02.2012
comment
для последовательного увеличения d до 4 [...] ? да, это все. Или вы можете заменить d на private static final AtomicInteger d = new AtomicInteger(0);, а затем увеличить на d.incrementAndGet(), что является операцией атомарного увеличения (в качестве упражнения: взгляните на JavaDocs для AtomicInteger и попробуйте реализовать свою собственную версию класса - вы, вероятно, закончите с использованием большого количества synchronized; фактическая реализация AtomicInteger не использует синхронизацию, она использует неблокирующую синхронизацию, что является очень интересной темой! ищите compare-and-set). - person Bruno Reis; 16.02.2012
comment
Насчет статьи, которую вы упомянули, я бы сказал, что вам лучше бежать от нее! Я прочитал страницу, на которую вы ссылаетесь, и ту, на которую есть ссылка оттуда. Это очень неточно (а точность абсолютно необходима при обсуждении концепций многопоточности), автор, похоже, не понимает, о чем говорит. Если вам нужен хороший текст о многопоточности, попробуйте каноническую книгу Java Concurrency in Practice: язык не очень сложный и очень точный, в нем много примеров из реальной жизни (не class Animal, class Dog extends Animal какая-то ерунда). - person Bruno Reis; 16.02.2012

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

Одна достойная ссылка находится здесь

person Hovercraft Full Of Eels    schedule 16.02.2012

volatile может сделать общий доступ безопасным (если достаточно атомарности одной операции чтения или записи), он не вызывает общий доступ.

Обратите внимание, что если вы сделаете d static, на самом деле не указано, какое значение будет иметь d, потому что оператор d = d + 1 не является атомарным, то есть поток может быть прерван между чтением и записью d. Типичными решениями для этого являются синхронизированный блок или AtomicInteger.

person meriton    schedule 16.02.2012