Подробная семантика изменчивости относительно своевременности видимости
Рассмотрим volatile int sharedVar
, Мы знаем, что JLS дает нам следующие гарантии:
- каждое действие пишущей нити
w
предшествует его записи стоимостиi
вsharedVar
в порядке программыhappens-before
действие записи; - запись стоимости
i
отw
happens-before
успешное чтениеi
отsharedVar
по теме чтенияr
; - успешное чтение
i
отsharedVar
по теме чтенияr
happens-before
все последующие действияr
в программном порядке.
Тем не менее, по-прежнему не гарантируется время на настенные часы, когда поток чтения будет наблюдать значение i
, Реализация, которая просто не позволяет потоку чтения увидеть это значение, все еще соответствует этому контракту.
Я думал об этом некоторое время, и я не вижу никаких лазеек, но я предполагаю, что должно быть. Пожалуйста, укажите на лазейку в моих рассуждениях.
6 ответов
Оказывается, что ответы и последующие дискуссии только закрепили мои первоначальные рассуждения. Теперь у меня есть кое-что на пути доказательства:
- возьмем случай, когда поток чтения выполняется полностью до того, как поток записи начинает выполняться;
- обратите внимание на порядок синхронизации, созданный этим конкретным прогоном;
- Теперь сдвиньте потоки во время настенных часов, чтобы они выполнялись параллельно, но сохраняли тот же порядок синхронизации.
Поскольку в модели памяти Java нет ссылок на время настенных часов, никаких препятствий этому не будет. Теперь у вас есть два потока, выполняющихся параллельно с потоком чтения, который не наблюдает никаких действий, выполняемых потоком записи. QED.
Пример 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 фактически должна была бы предсказать, что Поток A напишет в момент 14, чтобы знать, что позволить Потоку B читать в момент 1. Достоверность и даже выполнимость этого весьма сомнительна.
Из этого мы можем определить следующую, реалистичную свободу, которую может реализовать реализация JVM:
Видимость любой непрерывной последовательности действий разъединения потоком может быть безопасно отложена до тех пор, пока действие захвата не прервет его.
Условия выпуска и приобретения определены в JLS §17.4.4.
Следствием этого правила является то, что действия потока, который только пишет и никогда ничего не читает, могут быть отложены на неопределенное время без нарушения отношения " происходит до".
Прояснение изменчивой концепции
volatile
Модификатор на самом деле о двух разных понятиях:
- Твердая гарантия того, что действия на нем будут соблюдаться до совершения заказа;
- Мягкое обещание лучших усилий времени выполнения к своевременной публикации записей.
Обратите внимание, что пункт 2. не определен JLS никоим образом, он просто возникает из общего ожидания. Очевидно, что реализация, которая нарушает обещание, все еще соответствует требованиям. Со временем, когда мы перейдем к массивно параллельным архитектурам, это обещание может оказаться весьма гибким. Поэтому я ожидаю, что в будущем соотношение гарантии с обещанием окажется недостаточным: в зависимости от требования нам понадобится одно без другого, одно с другим видом другого или любое количество других комбинаций.
Вы отчасти правы. Я понимаю, что это было бы законно, хотя и только если поток r
не участвовал ни в каких других операциях, которые имели отношение "происходит до" относительно потока w
,
Таким образом, нет гарантии того, когда с точки зрения времени настенных часов; но есть гарантия в отношении других точек синхронизации в программе.
(Если это вас беспокоит, учтите, что в более фундаментальном смысле нет никакой гарантии, что JVM когда-либо действительно выполнит какой-либо байт-код своевременно. JVM, которая просто остановилась навсегда, почти наверняка была бы законной, потому что по сути невозможно предоставить жесткие сроки гарантии на исполнение.)
Пожалуйста, смотрите этот раздел (17.4.4). Вы немного изменили спецификацию, что вас смущает. спецификация чтения / записи для изменчивых переменных ничего не говорит о конкретных значениях, а именно:
- Запись в энергозависимую переменную (§8.3.1.4) v синхронизирует со всеми последующими чтениями v любым потоком (где последующие определены в соответствии с порядком синхронизации).
ОБНОВИТЬ:
Как упоминает @AndrzejDoyle, вы можете иметь нить r
читать устаревшее значение до тех пор, пока поток не выполнит ничего, после чего точка устанавливает точку синхронизации с потоком w
на более позднем этапе исполнения (как тогда вы будете в нарушение спецификации). Так что да, там есть комната для маневра, но нить r
будет очень ограничено в том, что он может делать (например, запись в System.out установит более позднюю точку синхронизации, так как большинство потоковых импульсов синхронизированы).
Я не верю ни одному из нижеприведенного. Все сводится к значению "последующий", который не определен, за исключением двух упоминаний в 17.4.4, где он тавтологически "определен в соответствии с порядком синхронизации".)
Единственное, что нам действительно нужно сделать, это раздел 17.4.3:
Последовательная согласованность является очень сильной гарантией, которая дается в отношении видимости и порядка выполнения программы. В последовательном последовательном выполнении существует общий порядок для всех отдельных действий (таких как чтение и запись), который согласуется с порядком программы, и каждое отдельное действие является атомарным и сразу же видимым для каждого потока. (выделение добавлено)
Я думаю, что есть такая гарантия в реальном времени, но вы должны собрать ее воедино из различных разделов главы 17 JLS.
- В соответствии с разделом 17.4.5 "отношение" происходит до "определяет, когда происходят гонки данных". Кажется, это не указано явно, но я предполагаю, что это означает, что если действиеa происходит перед другим действиемa ', между ними нет гонки данных.
- В соответствии с 17.4.3: "Набор действий последовательно непротиворечив, если... каждое чтениеr переменнойv видит значение, записанное записьюw вv таким образом, что w предшествует r в порядке выполнения... Если a В программе нет данных о гонках, тогда все исполнения программы будут выглядеть последовательно согласованными ".
Если вы пишете в переменную volatile v
и впоследствии читать из него в другом потоке, это означает, что запись происходит до чтения. Это означает, что нет никакой гонки данных между записью и чтением, а это означает, что они должны быть последовательно согласованными. Это означает, что чтениеr должно видеть значение, записанное записьюw(или последующей записью).
Я думаю volatile
в Java это выражается в терминах "если вы увидите A, вы также увидите B".
Чтобы быть более явным, Java обещает, что при потоковом чтении переменная переменная foo
и видит значение A, у вас есть некоторые гарантии относительно того, что вы увидите, когда позже прочитаете другие переменные в том же потоке. Если тот же поток, который написал A для foo
также написал Б bar
(перед тем как написать foo
), вы гарантированно увидите хотя бы B в bar
,
Конечно, если вы никогда не увидите А, вы не можете быть уверены, что увидите Б тоже. И если вы видите B в bar
, что ничего не говорит о видимости А в foo
, Кроме того, время, которое проходит между потоком, записывающим foo
и еще одна нить видя А в foo
не гарантируется
Там не должно быть лазейка. Это действительно теоретически "законно" для реализации JVM, которая сделала это. Точно так же теоретически "законно" никогда не планировать поток, имя которого начинается с "X"
, Или внедрите JVM, которая никогда не запускает GC.
Но на практике реализации JVM, которые вели себя подобным образом, не нашли бы никакого одобрения.
на самом деле это не так, пожалуйста, смотрите спецификацию, на которую я ссылаюсь в моем ответе.
Ах да, это так!
Реализация, которая навсегда заблокировала поток в чтении, будет технически совместима с JLS 17.4.4. "Последующее чтение" никогда не завершается.