Понимание происходит-до и синхронизация

Я пытаюсь понять Java происходит до порядка, и есть несколько вещей, которые кажутся очень запутанными. Насколько я могу судить, «случается раньше» — это просто приказ о наборе действий и не дает никаких гарантий о порядке выполнения в реальном времени. На самом деле (выделение мое):

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

Итак, все, что он говорит, это то, что если есть два действия w (запись) и r (чтение) такие, что hb(w, r), то r может на самом деле происходит до w при выполнении, но нет никакой гарантии, что это произойдет. Также запись w наблюдается чтением r.

Как я могу определить, что два действия выполняются последовательно во время выполнения? Например:

public volatile int v;
public int c;

Действия:

Thread A
v = 3;  //w

Thread B
c = v;  //r

Здесь у нас есть hb(w, r), но это не значит, что c будет содержать значение 3 после присваивания. Как обеспечить, чтобы c было присвоено значение 3? порядок синхронизации дают такие гарантии?


person St.Antario    schedule 03.04.2016    source источник


Ответы (4)


Когда JLS говорит, что какое-то событие X в потоке A устанавливает отношение произошло раньше с событием Y в потоке B, это не означает, что X произойдет раньше Y.

Это означает, что ЕСЛИ X происходит до Y, то оба потока согласятся, что X произошло до Y. Другими словами, оба потока увидят память программы в состоянии, которое согласуется с тем, что X происходит до Y.


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

Это прекрасно работает, когда потоки не взаимодействуют друг с другом, но вызывает проблемы, когда они действительно хотят взаимодействовать: если поток A сохраняет значение в обычную переменную, Java не гарантирует, когда (или даже if) поток B увидит изменение значения.

Чтобы решить эту проблему, когда это необходимо, Java предоставляет определенные средства синхронизации потоков. То есть заставить потоки согласовать состояние памяти программы. Ключевое слово volatile и ключевое слово synchronized — это два способа установления синхронизации между потоками.


Я думаю, что причина, по которой они назвали это «происходит раньше», состоит в том, чтобы подчеркнуть транзитивный характер отношения: если вы можете доказать, что A происходит до B, и вы можете доказать, что B происходит до C, то в соответствии с правилами, указанными в JLS , вы доказали, что А происходит раньше, чем С.

person Solomon Slow    schedule 03.04.2016
comment
JLS, 17.4.5 говорит: Если одно действие происходит раньше другого, то первое видно и упорядочено перед вторым. Поэтому я думаю, что утверждение, что ЕСЛИ X происходит раньше Y, будет ошибочным, тогда оба потока согласятся.... Скорее, "происходит раньше" на самом деле означает, во всех смыслах и целях, "X" происходит раньше "Y". Некоторое переупорядочивание разрешено, если переупорядочивание приводит к результатам, совместимым с законным исполнением (опять же JLS), но это не означает, что происходит до того, как отношения будут обусловлены чем-либо. - person Just Me; 15.12.2020
comment
Чуть ниже: разблокировка монитора происходит перед каждой последующей блокировкой этого монитора. что неявно условно (зависит от порядка блокировки и разблокировки). Но кажется, что понятие «происходит раньше» относится к безусловной связи, хотя и полностью раскрываемой только во время выполнения. Как человек, который только начинает заниматься этим, я нашел ваш ответ поучительным, но потом понял, что он сбивает с толку, пока, наконец, не решил просто прочитать JLS. Я думаю, что разъяснение этого момента в вашем ответе может избавить будущих читателей от некоторых проблем... - person Just Me; 16.12.2020
comment
Я хочу сказать, что это не так вводит в заблуждение, как вы это представляете. На самом деле это означает, что это происходит раньше (вплоть до некоторой свободы переупорядочения, которую вы гарантированно не заметите). - person Just Me; 16.12.2020
comment
Хорошо, я не собираюсь менять свой ответ, потому что весь вопрос закрыт. Но имело бы для вас больше смысла, если бы во втором абзаце я сказал: «Это означает, что ЕСЛИ событие X на самом деле происходит в действительности- время до того, как произойдет событие Y, оба потока согласятся с фактическим порядком двух событий? Другими словами, было ли ваше замешательство вызвано тем, что я использовал фразу «происходит раньше» для описания одного события, происходящего перед другим событием, вместо того, чтобы использовать эту фразу как имя особого отношения, как JLS использует его? - person Solomon Slow; 16.12.2020
comment
не совсем. Насколько я понимаю, отношение X происходит до Y, это означает, что любой тип эксперимента (в любом потоке) с участием X и Y будет думать, что X произошло до Y. IF, который вы вводите, не нужен и фактически вводит в заблуждение. : X может даже произойти после Y в реальном времени, но только в том случае, если любому, кто потрудится посмотреть, покажется, что X произошло до Y. Может помочь следующий пример (снова JLS): запись значения по умолчанию значение для каждого поля объекта, созданного потоком, не обязательно должно происходить до начала этого потока, пока ни одно чтение никогда не наблюдает этот факт. - person Just Me; 16.12.2020

Я хотел бы связать приведенное выше утверждение с некоторым примером потока кода.

Чтобы понять это, давайте возьмем ниже класс, который имеет два поля counter и isActive.

class StateHolder {
    private int counter = 100;
    private boolean isActive = false;

    public synchronized void resetCounter() {
            counter = 0;
            isActive = true;
    }

    public synchronized void printStateWithLock() {
        System.out.println("Counter : " + counter);
        System.out.println("IsActive : " + isActive);
    }

    public void printStateWithNoLock() {
        System.out.println("Counter : " + counter);
        System.out.println("IsActive : " + isActive);
    }
}

И предположим, что есть три потока T1, T2, T3, вызывающие следующие методы для одного и того же объекта StateHolder:

T1 вызывает resetCounter(), а T2 вызывает printStateWithLock() одновременно, и T1 получает блокировку
T3 -> вызывает printStateWithNoLock() после того, как T1 завершил свое выполнение.

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

и непосредственная линия говорит,

Согласно приведенному выше утверждению, это дает возможность JVM, ОС или базовому оборудованию изменять порядок операторов в методе resetCounter(). И по мере выполнения T1 он может выполнять операторы в следующем порядке.

    public synchronized void resetCounter() {
            isActive = true;
            counter = 0;
    }

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

Теперь, если посмотреть на это с точки зрения T2, это переупорядочивание не имеет негативного влияния, потому что и T1, и T2 синхронизируются с одним и тем же объектом, и T2 гарантированно увидит изменения в обоих полях, независимо от того, было ли переупорядочение случилось или нет, как там бывает-прежде отношения. Таким образом, вывод всегда будет:

Counter : 0
IsActive : true

Это соответствует утверждению: Если изменение порядка приводит к результатам, согласующимся с законным исполнением, оно не является незаконным.

Но посмотрите на это с точки зрения T3, при таком переупорядочении возможно, что T3 увидит обновленное значение isActive как «truebut still see thecountervalue as100», хотя T1 завершил свое выполнение.

Counter : 100
IsActive : true

Следующий пункт в приведенной выше ссылке дополнительно разъясняет утверждение и говорит, что:

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

В этом примере T3 столкнулся с этой проблемой, так как она не имеет отношения «происходит до» с T1 или T2. Это встроено в Не обязательно должно казаться, что это произошло в этом порядке для любого кода, с которым они не имеют общего отношения "происходит до".

ПРИМЕЧАНИЕ. Для упрощения случая у нас есть один поток T1, изменяющий состояние, и T2 и T3, считывающие состояние. можно иметь

T1 обновляет counter to 0, позже
T2 изменяет isActive на true и видит counter is 0, через некоторое время
T3, который печатает состояние, все еще может видеть only isActive as true but counter is 100, хотя и T1, и T2 завершили выполнение.

Что касается последнего вопроса:

у нас есть hb(w, r), но это не значит, что c будет содержать значение 3 после присваивания. Как обеспечить, чтобы c присваивалось значение 3?

public volatile int v;
public int c;

Thread A
v = 3;  //w

Thread B
c = v;  //r

Поскольку v является изменчивым, согласно Происходит перед заказом

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

Таким образом, можно с уверенностью предположить, что когда поток B пытается прочитать переменную v, он всегда будет читать обновленное значение, а c будет присвоено значение 3 в приведенном выше коде.

person Madhusudana Reddy Sunnapu    schedule 03.04.2016
comment
Один вопрос. Почему вы не объявили поля volatile? Безопасно ли использовать их таким образом? Я думаю, что поток, который выполняет построение объекта... Я имею в виду изменения, сделанные потоком ( counter = 100 ), могут не наблюдаться ни одним потоком, вызывающим метод объекта (до отношений ничего не происходит). Может я не совсем понял твою мысль...? - person St.Antario; 03.04.2016
comment
@St.Antario JLS #8.8.3 указывают, что он заблокирует строящийся объект, который обычно недоступен для других потоков, пока все конструкторы объекта не завершат свою работу. Итак, согласно приведенной выше ссылке, потоки будут видеть только полностью инициализированный объект только после завершения конструктора, и JVM позаботится об этом. Если только мы не опубликуем объект в конструкторе и не сделаем его экранированным, как в разделе Publication and Escape книги Java concurrency in practice. - person Madhusudana Reddy Sunnapu; 03.04.2016
comment
Да, он блокируется, но четко не указано, что блокировка действует именно как synchronized { ... }. Я думал, они имели в виду, что операция построения атомарна и все. Никаких гарантий видимости памяти не предусмотрено. - person St.Antario; 03.04.2016
comment
@St.Antario Spec говорит, что это заблокирует строящийся объект. Хотя это и не является явным, я бы прочитал это так: наличие блокировки гарантирует, что она атомарна, а также гарантирует видимость памяти. Таким образом, как только конструктор завершит инициализацию объекта, другие потоки увидят полностью инициализированное внутреннее состояние объекта. - person Madhusudana Reddy Sunnapu; 04.04.2016
comment
К сожалению, мы не можем так сказать. Я проконсультировался с JCIP, раздел 3.5 Безопасная публикация, и там был пример использования небезопасной публикации Holder (имеет единственное общедоступное поле int). Говорят, что но, что еще хуже, другие потоки могут видеть актуальное значение для ссылки на держатель, но устаревшие значения для состояния Holder. - person St.Antario; 04.04.2016
comment
@St.Antario Это правда. На самом деле, если вы посмотрите на мой предыдущий комментарий, он говорит: Если мы не опубликуем объект в конструкторе и не сделаем его экранированным, как в разделе «Публикация и экранирование» книги «Параллелизм Java на практике». Это не проблема с конструктором, но ссылка на объект holder не публикуется безопасно. Но в вашем примере StateHolder не экранируется. Мы создаем объект StateHolder и передаем его потокам T1, T2 и T3. - person Madhusudana Reddy Sunnapu; 04.04.2016
comment
Да, это не так. Но я все еще не совсем согласен с тем, что даже если мы опубликуем объект в потоке, используя синхронизацию по внутренней блокировке объекта, мы гарантированно увидим свежие значения его полей. К сожалению, это четко не указано в JLS. Вы сказали, что блокировка используется при создании объекта, но какая именно? Итак, я бы сказал, что мы должны объявить поля либо final, либо volatile, чтобы обеспечить видимость правильных значений. Кроме того, JLS 17.5 . Такие гарантии являются именно final семантикой поля. - person St.Antario; 05.04.2016
comment
@St.Antario Per JLS#8.8.3 он заблокирует строящийся объект, который обычно недоступен для других потоков, пока все конструкторы объекта не завершат свою работу , если только объект не исчезнет, Я все еще чувствую или интерпретирую final, возможно, не требуется, чтобы конструктор публиковал полностью построенное состояние объекта, хотя это явно не указано. Если я найду что-то более конкретное в любом случае, поделюсь, и это может помочь. - person Madhusudana Reddy Sunnapu; 05.04.2016
comment
@St.Antario St.Antario Я просмотрел исходный код StringBuffer и ThreadGroup, чтобы узнать, нужно ли помечать переменные final. Оказалось, что они не сделали поля окончательными, хотя они инициализированы в constructor. Это дает хорошее указание на то, что нет необходимости помечать поля как final, если это не требуется, а создание объекта с использованием конструктора гарантирует видимость памяти и гарантии атомарности. А также пометка поля как final позволяет не изменять значение позже, что в большинстве случаев может рассматриваться как ограничение, если мы хотим позже изменить его значение через сеттеры. - person Madhusudana Reddy Sunnapu; 05.04.2016
comment
У меня есть вопрос о вашем объяснении термина не обязательно подразумевающего, что они должны иметь место в этом порядке в реализации. В вашем примере я могу вывести, что counter = 0; происходит до того, как isActive = true;. Но почему? В jsl есть пример под этим термином. **Например, запись значения по умолчанию в каждое поле объекта, созданного потоком, не обязательно должна происходить до начала этого потока, если ни одно чтение никогда не наблюдает этот факт. **В вашем примере другая ситуация. - person Chaojun Zhong; 27.07.2017
comment
Таким образом, можно с уверенностью предположить, что когда поток B попытается прочитать переменную v, он всегда будет читать обновленное значение, а c будет присвоено значение 3 в приведенном выше коде. — это неверно. Это может быть c = 0. - person v.ladynev; 28.01.2018

Интерпретация ответа @James по моему вкусу:

// Definition: Some variables
private int first = 1;
private int second = 2;
private int third = 3;
private volatile boolean hasValue = false;

// Thread A
first = 5;
second = 6;
third = 7;
hasValue = true;

// Thread B
System.out.println("Flag is set to : " + hasValue);
System.out.println("First: " + first);  // will print 5
System.out.println("Second: " + second); // will print 6
System.out.println("Third: " + third);  // will print 7

если вы хотите, чтобы состояние/значение памяти (памяти и кеша ЦП) отображалось во время оператора записи переменной одним потоком,

Состояние памяти, наблюдаемое hasValue=true (оператор записи) в потоке A:

first со значением 5, second со значением 6, third со значением 7

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

Если X (hasValue=true) в потоке A происходит до Y (sysout(hasValue)) в потоке B, поведение должно быть таким, как если бы X произошло до Y в том же потоке (значения памяти, видимые в X, должны быть одинаковыми, начиная с Y)

person user104309    schedule 13.03.2017
comment
dzone.com/articles/ - person user104309; 13.03.2017

Здесь у нас есть hb(w, r), но это не значит, что c будет содержать значение 3 после присваивания. Как обеспечить, чтобы c присваивалось значение 3? Дает ли порядок синхронизации такие гарантии?

И ваш пример

public volatile int v;
public int c;
Actions:

Thread A
v = 3;  //w

Thread B
c = v;  //r

Вам не нужно volatile для v в вашем примере. Рассмотрим аналогичный пример

int v = 0;
int c = 0;
volatile boolean assigned = false;

Действия:

Тема А

v = 3;
assigned = true;

Поток B

while(!assigned);
c = v;
  1. Поле assigned является непостоянным.
  2. У нас будет оператор c = v в Thread B только после того, как assigned станет true (за это отвечает while(!assigned)).
  3. если у нас есть volatile — у нас есть happens before.
  4. happens before означает, что если мы увидим assigned == true — мы увидим все, что было до оператора assigned = true: мы увидим v = 3.
  5. Итак, когда у нас есть assigned == true -> у нас есть v = 3.
  6. В результате имеем c = 3.

Что произойдет без volatile

int v = 0;
int c = 0;
boolean assigned = false;

Действия:

Тема А

v = 3;
assigned = true;

Поток B

while(!assigned);
c = v;

У нас пока assigned без volatile.

В такой ситуации значение c в Thread B может быть равно 0 или 3. Так что нет никаких гарантий, что c == 3.

person v.ladynev    schedule 28.01.2018