Я читаю главу 17. Потоки и блокировки JLS, и следующее утверждение о последовательной согласованности в Java кажется мне неверным:

Если в программе нет гонок данных, то все выполнения программы будут выглядеть последовательно последовательными.

Они определяют гонку данных следующим образом:

Когда программа содержит два конфликтующих доступа (§17.4.1), которые не упорядочены отношениями happens-before, говорят, что она содержит гонку данных.

Они определяют конфликтные доступы как:

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

И наконец, у них есть следующее об отношениях "происходит-до":

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

Моя проблема с первым утверждением заключается в том, что я думаю, что могу придумать программу на Java, которая не имеет гонок данных и допускает последовательное непоследовательное выполнение:

// Общий код
volatile int vv = 0;
int v1 = 0;
int v2 = 0;


// Поток1 Поток2
   v1 = 1;
   v2 = 2;
   vv = 10; while(vv == 0) {;}
                 int r1 = v1;
                 int r2 = v2;
                 System.out.println("v1=" + r1 + " v2=" + r2);
   v1 = 3;
   v2 = 4;
   vv = 20;

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

Как я понимаю, эта программа:

  • не имеет гонок данных: чтения v1 и v2 в Thread2 синхронизированы с записями в Thread1
  • может выводить v1=1 v2=4 (что нарушает последовательную согласованность)
  • .

В результате, первоначальное заявление JLS

Если в программе нет гонок данных, то все выполнения программы будут выглядеть последовательно последовательными.

кажется мне неверным.

Я что-то упустил или где-то допустил ошибку?

EDIT: Пользователь chrylis-cautiouslyoptimistic правильно указал, что приведенный мной код может выводить v1=1 v2=4 с последовательной связностью - просто строки в коде потоков должны чередоваться немного по-другому.

Здесь представлен слегка измененный код (я изменил порядок чтения), для которого последовательная согласованность не может вывести v1=1 v2=4, но все по-прежнему применимо.

// Общий код
volatile int vv = 0;
int v1 = 0;
int v2 = 0;


// Поток1 Поток2
   v1 = 1;
   v2 = 2;
   vv = 10; while(vv == 0) {;}
                 int r2 = v2;
                 int r1 = v1;
                 System.out.println("v1=" + r1 + " v2=" + r2);
   v1 = 3;
   v2 = 4;
   vv = 20;

JavaCur

Ответов: 2

Ответы (2)

Ваша ошибка в пункте #1: Чтения v1 и v2 не синхронизированы.

Существуют отношения происходит-до, созданные только взаимодействием с vv, поэтому, например, в данном случае, если бы вы добавили vv в начало оператора print, вы бы гарантированно не увидели vv=20,v2=4. Поскольку вы заняты ожиданием того, что vv станет ненулевым но затем больше не взаимодействуете с ним, единственной гарантией является то, что вы увидите все эффекты, произошедшие до того, как он стал ненулевым (присвоение 1 и 2). Вы можете также увидеть будущие эффекты, потому что у вас нет никаких дальнейших случаев, предшествующих.

Даже если вы объявите все переменные как volatile, все равно можно вывести v1=1,v2=4 потому что многопоточные обращения к переменным не имеют определенного порядка, и глобальная последовательность может выглядеть так:

.
  1. T1: запись v1=1
  2. T1: write v2=2
  3. T1: write vv=10 (Поток 2 не может выйти из цикла while раньше этого места и гарантированно увидит все эти эффекты)
  4. .
  5. T2: read vv=10
  6. T2: прочитать v1=1
  7. T1: запись v1=3
  8. T1: запись v2=4
  9. T2: читать v2=4

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

Формально, гонка данных, определенная JLS, состоит из операций T1(write v1=3) и T2(read v1) (и второй гонки данных на v2). Это конфликтующие доступы (поскольку доступ T1 - это запись), но хотя оба эти события происходят после T2(read vv), они не упорядочены по отношению друг к другу.

На самом деле доказать, что вы ошибаетесь, гораздо проще, чем вы думаете. Действия между двумя независимыми потоками "синхронизируются с" по очень специальным правилам, все они определены в соответствующей главе в JSL. Принятый ответ гласит, что synchronizes-with не является фактическим термином, но это неверно. (если только я неправильно понял замысел или в нем нет ошибки).

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

Вы упоминаете volatile, но в то же время будьте внимательны к тому, что означает subsequent в этом:

Запись в изменчивое поле происходит перед каждым последующим чтением этого поля.

Это означает чтение, которое будет наблюдать за записью.


Если вы измените свой код и установите отношения synchronizes-with и неявные отношения happens-before следующим образом:

  v1 = 1;
  v2 = 2;
  vv = 10; 

             if(vv == 10) {
                int r1 = v1;
                int r2 = v2;
                // What are you allowed to see here?
             }

Вы можете начать рассуждать о том, что можно увидеть внутри блока if. Начните с простого, отсюда:

.

Если x и y - действия одного и того же потока и x идет раньше y в программном порядке, то hb(x, y).

OK, итак, v1 = 1 happens-before v2 = 2 и happens-before vv = 10. Таким образом мы устанавливаем hb между действиями в одном потоке.

Мы можем "синхронизировать" разные потоки через порядок synchronizes-with, через правильную главу и правильное правило:

Запись в переменную v синхронизируется со всеми последующими чтениями v любым потоком

.

Таким образом, мы установили порядок SW между двумя независимыми потоками. Это, в свою очередь, позволяет нам построить HB (так было раньше) теперь, благодаря правильной главе и еще одному правильному правилу:

Если действие x синхронизируется с следующим действием y, то мы также имеем hb(x, y).

Так что теперь у вас есть цепочка:

        (HB)          (HB)            (HB)                (HB)
v1 = 1 -----> v2 = 2 -----> vv = 10 ------> if(vv == 10) -----> r1 = v1 ....

Так что только теперь у вас есть доказательство того, что если блок будет читать r1 = 1 и r2 = 2. А поскольку volatile обеспечивает последовательную согласованность (никаких гонок данных), каждый поток, который прочитает vv как 10, наверняка также прочитает v1 как 1 и v2 как 2.

2022 WebDevInsider