Летучие в java

Насколько я знаю, волатильная запись бывает-до volatile read, поэтому мы всегда будем видеть самые свежие данные в переменной volatile. Мой вопрос в основном касается термина происходит до и где это происходит? Я написал часть кода, чтобы уточнить мой вопрос.

class Test {
   volatile int a;
   public static void main(String ... args) {
     final Test t = new Test();
     new Thread(new Runnable(){
        @Override
        public void run() {
            Thread.sleep(3000);
            t.a = 10;
        }
     }).start();
     new Thread(new Runnable(){
        @Override
        public void run() {
            System.out.println("Value " + t.a);
        }
     }).start();
   }
}

(блок try catch для ясности опущен)

В этом случае я всегда вижу, что значение 0 должно быть напечатано на консоли. Без Thread.sleep(3000); я всегда вижу значение 10. Является ли это случаем-до отношения или он печатает значение "10", потому что поток 1 запускает бит более ранний поток 2?

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

Ответ 1

Вы видите значение 0, потому что чтение выполняется перед записью. И вы видите значение 10, потому что запись выполняется перед чтением.

Если вы хотите иметь тест с более непредсказуемым результатом, вы должны обеими вашими потоками ждать CountDownLatch, чтобы они запускались одновременно:

final CountDownLatch latch = new CountDownLatch(1);
new Thread(new Runnable(){
    @Override
    public void run() {
        try {
            latch.await();
            t.a = 10;
        }
        catch (InterruptedException e) {
            // end the thread
        }
    }
 }).start();
 new Thread(new Runnable(){
    @Override
    public void run() {
        try {
            latch.await();
            System.out.println("Value " + t.a);
        }
        catch (InterruptedException e) {
            // end the thread
        }
    }
 }).start();
 Thread.sleep(321); // go
 latch.countDown();

Ответ 2

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

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

Например

volatile int a = 0;
int b = 0;

Тема 1:

b = 10;
a = 1;

Тема 2:

while(a != 1);
if(b != 10)
  throw new IllegalStateException();

Модель памяти Java говорит, что b должна всегда равняться 10, потому что энергонезависимое хранилище происходит перед энергозависимым хранилищем. И все записи, которые происходят в одном потоке до того, как произойдет энергозависимое хранилище, - до всех последующих летучих нагрузок.

Ответ 3

не придерживаться термина "случится раньше". это отношение между событиями, используемое jvm во время планирования операций R/W. на этом этапе это не поможет вам понять изменчивость. дело в следующем: jvm заказывает все операции R/W. jvm может заказывать, однако он хочет (конечно, подчиняться всем синхронизировать, блокировать, ждать и т.д.). и теперь: если переменная изменчива, тогда любая операция чтения увидит результат последней операции записи. если переменная нестабильна, то она не гарантируется (в разных потоках). что все

Ответ 4

Я переформулировал (изменяет жирный шрифт) правило, описанное выше в первом предложении вашего вопроса, как показано ниже, чтобы было лучше понято -

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

  • Также важно отметить, что волатильная запись/чтение всегда происходят с/из основной памяти и НЕ в/из любой локальной памяти ресурсы, такие как регистры, кэши процессора и т.д.

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

Напротив, все потоки, которые делятся энергонезависимой переменной, могут видеть разные значения в любой данный момент времени, если она не синхронизирована никакими другими механизмами синхронизации, такими как синхронизированный блок /method, ключевое слово final и т.д.

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

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

Надеюсь, что приведенное выше объяснение ясно:)

Ответ 5

piotrek прав, вот тест:

class Test {
   volatile int a = 0;
   public static void main(String ... args) {
     final Test t = new Test();
     new Thread(new Runnable(){
        @Override
        public void run() {
            try {
                Thread.sleep(3000);
            } catch (Exception e) {}
            t.a = 10;
            System.out.println("now t.a == 10");
        }
     }).start();
     new Thread(new Runnable(){
        @Override
        public void run() {
            while(t.a == 0) {}
            System.out.println("Loop done: " + t.a);
        }
     }).start();
   }
}

с изменчивым: он всегда заканчивается

без изменчивости: он никогда не закончится

Ответ 6

Из wiki:

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

Итак, если поток A напишет t.a со значением 10, а поток B попытается прочитать t.a, то более поздняя, ​​произойдет-до отношения гарантирует, что поток B должен прочитать значение 10, написанное потоком A, а не любое другое значение. Это естественно, так же как Алиса покупает молоко и помещает их в холодильник, затем Боб открывает холодильник и видит молоко. Однако, когда компьютер работает, доступ к памяти обычно не имеет прямого доступа к памяти, что слишком медленно. Вместо этого программное обеспечение получает данные из регистра или кеша, чтобы сэкономить время. Он загружает данные из памяти только в случае сбоя кеша. Это проблема.

Посмотрите код в вопросе:

class Test {
  volatile int a;
  public static void main(String ... args) {
    final Test t = new Test();
    new Thread(new Runnable(){ //thread A
      @Override
      public void run() {
        Thread.sleep(3000);
        t.a = 10;
      }
    }).start();
    new Thread(new Runnable(){ //thread B
      @Override
      public void run() {
        System.out.println("Value " + t.a);
      }
    }).start();
  }
}

Thread A записывает 10 в значение t.a, и поток B пытается его прочитать. Предположим, что поток A записывается до чтения нитки B, а затем, когда поток B читает, он загружает значение из памяти, потому что он не кэширует значение в регистре или кеше, поэтому он всегда получает 10, написанный потоком A. И если поток A записывается после поток B читает, поток B считывает начальное значение (0). Поэтому этот пример не показывает, насколько изменчивы работы и разница. Но если мы изменим код следующим образом:

class Test {
  volatile int a;
  public static void main(String ... args) {
    final Test t = new Test();
    new Thread(new Runnable(){ //thread A
      @Override
      public void run() {
        Thread.sleep(3000);
        t.a = 10;
      }
    }).start();
    new Thread(new Runnable(){ //thread B
      @Override
      public void run() {
        while (1) {
          System.out.println("Value " + t.a);
        }
      }
    }).start();
  }
}

Без volatile значение печати всегда должно быть начальным значением (0), даже некоторое чтение происходит после того, как поток A записывает 10 в t.a, что нарушает связь с предыдущим. Причина заключается в том, что компилятор оптимизирует код и сохраняет t.a в регистре, и каждый раз, когда он будет использовать значение регистра вместо чтения из кэш-памяти, конечно, это намного быстрее. Но это также вызывает проблему с ошибкой, возникающей перед обработкой, потому что поток B не может получить правильное значение после того, как другие обновят его.

В приведенном выше примере волатильная запись происходит до того, как волатильное считывание означает, что с изменчивым потоком B будет получать правильное значение t.a после того, как поток A обновит его. Компилятор будет гарантировать, что каждый раз, когда поток B считывает t.a, он должен читать из кеша или памяти, а не просто использовать значение устаревшего регистра.