Подтвердить что ты не робот

Подробная семантика изменчивости относительно своевременности видимости

Рассмотрим a volatile int sharedVar. Мы знаем, что JLS дает нам следующие гарантии:

  • каждое действие пишущего потока w перед его записью значения i в sharedVar в программном порядке happens-before действие записи;
  • запись значения i на w happens-before успешное чтение i из sharedVar с помощью потока чтения r;
  • успешное чтение i из sharedVar потоком чтения r happens-before всех последующих действий r в порядке выполнения программы.

Тем не менее, по-прежнему нет гарантии времени на настенные часы, указывающие на , когда поток чтения будет наблюдать значение i. Реализация, которая просто никогда позволяет потоку чтения видеть, что это значение по-прежнему соответствует этому контракту.

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

4b9b3361

Ответ 1

Оказывается, что ответы и последующие дискуссии только укрепили мои первоначальные рассуждения. У меня теперь есть что-то на пути доказательства:

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

Так как модель памяти Java не ссылается на время настенных часов, это не будет препятствием. Теперь у вас есть два потока, выполняемые параллельно с потоком чтения , не наблюдающим действий, выполняемых потоком записи. Что и требовалось доказать.

Пример 1: одна запись, один поток чтения

Чтобы сделать это открытие максимально острым и реальным, рассмотрите следующую программу:

static volatile int sharedVar;

public static void main(String[] args) throws Exception {
  final long startTime = System.currentTimeMillis();
  final long[] aTimes = new long[5], bTimes = new long[5];
  final Thread
    a = new Thread() { public void run() {
      for (int i = 0; i < 5; i++) {
        sharedVar = 1;
        aTimes[i] = System.currentTimeMillis()-startTime;
        briefPause();
      }
    }},
    b = new Thread() { public void run() {
      for (int i = 0; i < 5; i++) {
        bTimes[i] = sharedVar == 0?
            System.currentTimeMillis()-startTime : -1;
        briefPause();
      }
    }};
  a.start(); b.start();
  a.join(); b.join();
  System.out.println("Thread A wrote 1 at: " + Arrays.toString(aTimes));
  System.out.println("Thread B read 0 at: " + Arrays.toString(bTimes));
}
static void briefPause() {
  try { Thread.sleep(3); }
  catch (InterruptedException e) {throw new RuntimeException(e);}
}

Что касается JLS, это законный выход:

Thread A wrote 1 at: [0, 2, 5, 7, 9]
Thread B read 0 at: [0, 2, 5, 7, 9]

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

Пример 2: Два потока как чтение, так и запись

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

public static void main(String[] args) throws Exception {
  final long startTime = System.currentTimeMillis();
  final long[] aTimes = new long[5], bTimes = new long[5];
  final int[] aVals = new int[5], bVals = new int[5];
  final Thread
    a = new Thread() { public void run() {
      for (int i = 0; i < 5; i++) {
        aVals[i] = sharedVar++;
        aTimes[i] = System.currentTimeMillis()-startTime;
        briefPause();
      }
    }},
    b = new Thread() { public void run() {
      for (int i = 0; i < 5; i++) {
        bVals[i] = sharedVar++;
        bTimes[i] = System.currentTimeMillis()-startTime;
        briefPause();
      }
    }};
  a.start(); b.start();
  a.join(); b.join();
  System.out.format("Thread A read %s at %s\n",
      Arrays.toString(aVals), Arrays.toString(aTimes));
  System.out.format("Thread B read %s at %s\n",
      Arrays.toString(bVals), Arrays.toString(bTimes));
}

Чтобы помочь понять код, это будет типичный, реальный результат:

Thread A read [0, 2, 3, 6, 8] at [1, 4, 8, 11, 14]
Thread B read [1, 2, 4, 5, 7] at [1, 4, 8, 11, 14]

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

Thread A read [0, 1, 2, 3, 4] at [1, 4, 8, 11, 14]
Thread B read [5, 6, 7, 8, 9] at [1, 4, 8, 11, 14]

JVM на самом деле должен предсказать то, что Thread A будет писать в момент 14, чтобы знать, что позволить Thread B читать в момент 1. Правдоподобие и даже выполнимость этого довольно сомнительны.

Из этого можно определить следующую, реалистичную свободу, которую может реализовать реализация JVM:

Видимость любой непрерывной последовательности действий релиза потоком может быть безопасно перенесена до тех пор, пока не произойдет действие, которое прерывает его.

Термины release и приобретение определены в JLS §17.4.4.

Коррупция к этому правилу состоит в том, что действия потока, который только пишет и никогда ничего не читает, может быть отложен неопределенно, не нарушая отношения между событиями.

Очистка изменчивой концепции

Модификатор volatile фактически представляет собой два разных понятия:

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

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

Ответ 2

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

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

(Если это вас беспокоит, считайте, что в более фундаментальном смысле нет гарантии, что JVM когда-либо фактически выполнит какой-либо байт-код своевременно. JVM, который просто застопорился навсегда, почти наверняка будет легальным, поскольку он по существу невозможно обеспечить надежные гарантии времени при выполнении.)

Ответ 3

См. этот раздел (17.4.4). вы немного исказили спецификацию, что вас сбивает с толку. спецификация чтения/записи для изменчивых переменных ничего не говорит о конкретных значениях, в частности:

  • Запись в изменчивую переменную (§8.3.1.4) v синхронизируется со всеми последующими чтениями v любым потоком (где последующее определяется в соответствии с порядком синхронизации).

UPDATE:

Как упоминает @AndrzejDoyle, вы могли бы иметь поток r для чтения устаревшего значения, если ничто другое, что этот поток не делает после этой точки, устанавливает точку синхронизации с потоком w в какой-то более поздний момент выполнения (так как тогда вы будете нарушать спецификацию). Так что да, там есть место для маневра, но поток r был бы очень ограничен в том, что он мог бы сделать (например, запись в System.out установила бы более позднюю точку синхронизации, поскольку большинство потоков потока синхронизированы).

Ответ 4

Я больше не верю никому из ниже. Все сводится к значению "последующее", которое undefined, за исключением двух упоминаний в 17.4.4, где оно тавтологически "определено в соответствии с порядком синхронизации".)

Единственное, что нам действительно нужно сделать, - это раздел 17.4.3:

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

Я думаю, что существует такая гарантия в реальном времени, но вы должны собрать ее из разных разделов JLS Chapter 17.

  • В соответствии с разделом 17.4.5 "отношение" происхождение-до "определяет, когда происходят расы данных". Это, как представляется, явно не указано, но я предполагаю, что это означает, что если происходит действие a - перед другим действием a ', между ними нет расы данных.
  • Согласно 17.4.3: "Набор действий последовательно согласован, если... каждый прочитанный r переменной v видит значение, записанное в записи w в v, такое, что w приходит до r в порядке выполнения. Если в программе нет расов данных, то все исполнения программы будут последовательно последовательными."

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

Ответ 5

Там не должно быть лазейки. На самом деле теоретически "законным" является внедрение JVM, которая сделала это. Точно так же теоретически "законно" никогда не планировать поток, имя которого начинается с "X". Или реализовать JVM, который никогда не запускает GC.

Но на практике реализации JVM, которые вели себя такими способами, не нашли бы признания.


на самом деле это не так, см. спецификацию, на которую я ссылаюсь в своем ответе.

О да, это!

Реализация, которая навсегда заблокировала поток в чтении, будет технически совместима с JLS 17.4.4. "Последующее чтение" никогда не завершается.

Ответ 6

Я думаю, что volatile в Java выражается в терминах "если вы видите A, вы также увидите B".

Чтобы быть более явным, Java promises, когда поток читает переменную volatile foo и видит значение A, у вас есть некоторые гарантии относительно того, что вы увидите, когда читаете другие переменные позже в том же потоке. Если тот же поток, который написал A до foo, также написал B в bar (перед написанием A до foo), вы гарантированно увидите по крайней мере B в bar.

Конечно, если вы никогда не доберетесь до A, вам также не будет гарантировано увидеть B. И если вы видите B в bar, это ничего не говорит о видимости A в foo. Кроме того, время, которое проходит между потоком, записывающим A в foo, и другим потоком, видящим A в foo, не гарантируется.