Авторские права принадлежат автору Для любой формы перепечатки, пожалуйста, свяжитесь с автором для получения разрешения и укажите источник.
Что такое потокобезопасность?
Почему существует проблема безопасности потоков?
Когда несколько потоков совместно используют одну и ту же глобальную или статическую переменную одновременно, при выполнении операций записи могут возникать конфликты данных, то есть проблемы с безопасностью потоков. Однако при выполнении операции чтения проблем с конфликтом данных не возникает.
Случай:Теперь спрос составляет 100 билетов на поезд, и есть два окна для одновременного получения билетов. Пожалуйста, используйте многопоточность, чтобы смоделировать эффект захвата билетов.
public class ThreadTrain implements Runnable {
private int trainCount = 10;
@Override
public void run() {
while (trainCount > 0) {
try {
Thread.sleep(500);
sale();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void sale() {
if (trainCount > 0) {
--trainCount;
System.out.println(Thread.currentThread().getName() + ",出售第" + (10 - trainCount) + "张票");
}
}
public static void main(String[] args) {
ThreadTrain threadTrain = new ThreadTrain();
Thread t1 = new Thread(threadTrain, "1台");
Thread t2 = new Thread(threadTrain, "2台");
t1.start();
t2.start();
}
}
результат операции:
Окно 1 и Окно 2 продают билеты на девятый и девятый поезд одновременно, а некоторые билеты на поезд будут продаваться повторно. Делается вывод, что когда несколько потоков используют одну и ту же глобальную переменную-член, в операции записи могут возникать конфликты данных.
Поточное безопасное решение:
просить:Как решить проблему безопасности потоков между несколькими потоками
отвечать:Используйте синхронизацию между несколькими потоками или используйте блокировки.
просить:Зачем использовать синхронизацию потоков или использовать блокировки для решения проблем безопасности потоков?
отвечать:Может возникнуть проблема конфликта данных (проблема небезопасного потока), и может быть выполнен только текущий поток. После выполнения кода блокировка снимается, чтобы можно было выполнять другие потоки. Таким образом, проблема небезопасности потока может быть решена.
просить:Что такое синхронизация между потоками
отвечать:Когда несколько потоков совместно используют один и тот же ресурс, они не будут мешать другим потокам.
просить:Что такое многопоточная синхронизация
отвечать:Когда несколько потоков совместно используют один и тот же ресурс, они не будут мешать другим потокам.
встроенный замок
Java предоставляет встроенный механизм блокировки для поддержки атомарности. Каждый объект Java может использоваться в качестве блокировки для достижения синхронизации, называемой встроенной блокировкой. Поток автоматически получает блокировку перед входом в блок кода синхронизации, и выполнение блока кода завершается и завершается нормально. Или блокировка будет снята, когда блок кода выдает исключение и завершает работу.
Встроенная блокировка является блокировкой взаимного исключения, то есть после того, как поток A получает блокировку, поток B блокируется до тех пор, пока поток A не освободит блокировку, и поток B может получить ту же блокировку. Встроенные блокировки реализуются с помощью ключевого слова synchronized, которое можно использовать двумя способами:
- Измените метод, который необходимо синхронизировать (все методы, которые обращаются к переменным состояния, должны быть синхронизированы), в это время объект, который действует как блокировка, — это объект, который вызывает метод синхронизации.
- Блок кода синхронизации такой же, как метод, который необходимо синхронизировать напрямую с помощью синхронизированной модификации, но степень детализации блокировки может быть более тонкой, и объект, выступающий в роли блокировки, не обязательно является этим, но также и другими объектами, поэтому он более гибкий в использовании
синхронизированный блок синхронизированный
就是将可能会发生线程安全问题的代码,给包括起来。
synchronized(同一个数据){
可能会发生线程冲突问题
}
就是同步代码块
synchronized(对象)//这个对象可以为任意对象
{
需要被同步的代码
}
Объекты похожи на замки. Поток, удерживающий блокировку, может выполняться синхронно. Даже если поток, не удерживающий блокировку, получает право на выполнение ЦП, он не сможет войти. Предпосылка синхронизации:
- Должно быть два или более потока
- Несколько потоков должны использовать одну и ту же блокировку
Необходимо убедиться, что в синхронизации может работать только один поток.
выгода:Решающие проблемы с многопотативной безопасностью
Недостатки:Несколько потоков должны оценивать блокировку, которая потребляет ресурсы и захватывает ресурсы блокировки.
Пример кода:
private void sale() {
synchronized (this) {
if (trainCount > 0) {
--trainCount;
System.out.println(Thread.currentThread().getName() + ",出售第" + (10 - trainCount) + "张票");
}
}
}
Синхронный метод
Изменение синхронизированного метода называется синхронизированным методом.
public synchronized void sale() {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
Какая блокировка используется синхронным методом?
Ответ: Синхронизированные функции используют эту блокировку.
Метод доказательства: один поток использует синхронизированный блок кода (он заблокирован), а другой поток использует синхронизированную функцию. Если два потока не могут достичь синхронизации при захвате билетов, произойдет ошибка данных.
Ссылаться на:Использование и различие между блокировкой метода, блокировкой объекта и блокировкой класса
package com.itmayiedu;
class Thread0009 implements Runnable {
private int trainCount = 10;
private Object oj = new Object();
public boolean flag = true;
public void run() {
if (flag) {
while (trainCount > 0) {
synchronized (this) {
try {
Thread.sleep(10);
} catch (Exception e) {
// TODO: handle exception
}
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + "," + "出售第" + (10 - trainCount + 1) + "票");
trainCount--;
}
}
}
} else {
while (trainCount > 0) {
sale();
}
}
}
public synchronized void sale() {
try {
Thread.sleep(10);
} catch (Exception e) {
// TODO: handle exception
}
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + "," + "出售第" + (10 - trainCount + 1) + "票");
trainCount--;
}
}
}
public class Test009 {
public static void main(String[] args) throws InterruptedException {
Thread0009 threadTrain1 = new Thread0009();
Thread0009 threadTrain2 = new Thread0009();
threadTrain2.flag = false;
Thread t1 = new Thread(threadTrain1, "窗口1");
Thread t2 = new Thread(threadTrain2, "窗口2");
t1.start();
Thread.sleep(40);
t2.start();
}
}
функция статической синхронизации
Добавьте в метод ключевое слово static, измените его с помощью ключевого слова synchronized или используйте файл класса .class. Блокировка, используемая функцией статической синхронизации, представляет собой объект файла байт-кода, которому принадлежит функция. Его можно получить методом getClass или представить в виде имени текущего класса.class. Пример кода:
public static void sale() {
synchronized (ThreadTrain3.class) {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
}
Суммировать:
Блокировка, используемая синхронизированным измененным методом, является текущей блокировкой this.
Синхронизированный модифицированный статический метод использует блокировку файла байт-кода текущего класса.
Многопоточный тупик
Синхронизация вложена в синхронизацию, что приводит к блокировкам, которые нельзя снять.
public class ThreadTrain3 implements Runnable {
private int trainCount = 100;
private boolean flag = true;
@Override
public void run() {
if(flag){
while (true) {
// 如果flag为true先拿到obj锁,然后拿到this锁才能执行
synchronized(ThreadTrain3.class){
sale();
}
}
} else {
// 如果flag为false先拿到this锁,然后拿到obj锁才能执行
while(true){
sale();
}
}
}
public synchronized void sale() {
synchronized (ThreadTrain3.class) {
if (trainCount > 0) {
try{
Thread.sleep(40);
}catch(Exception e){}
System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "张票");
trainCount--;
}
}
}
public static void main(String[] args) {
ThreadTrain3 threadTrain = new ThreadTrain3();
Thread t1 = new Thread(threadTrain, "①号");
Thread t2 = new Thread(threadTrain, "②号");
t1.start();
Thread.sleep(40);
threadTrain.flag = false;
t2.start();
}
}
Что такое Threadlocal
ThreadLocal вызывает локальные переменные потока и обращается к потоку, который имеет свои собственные локальные переменные. При использовании ThreadLocal для поддержки переменных ThreadLocal предоставляет независимую копию переменной для каждого потока, использующего эту переменную, поэтому каждый поток может изменять свою собственную копию независимо, не затрагивая копии, соответствующие другим потокам. Метод интерфейса ThreadLocal Интерфейс класса ThreadLocal очень прост, всего 4 метода, сначала рассмотрим его:
- void set(значение объекта) Устанавливает значение локальной переменной текущего потока.
- public Object get() Этот метод возвращает локальную переменную потока, соответствующую текущему потоку.
- public void remove() удаляет значение локальной переменной текущего потока, чтобы уменьшить использование памяти, этот метод является новым методом в JDK 5.0. Следует отметить, что когда поток завершается, локальные переменные, соответствующие потоку, будут автоматически удалены сборщиком мусора, поэтому нет необходимости явно вызывать этот метод для очистки локальных переменных потока, но он может ускорить скорость. восстановления памяти.
- Защищенный объект initialValue() возвращает начальное значение локальной переменной потока Этот метод является защищенным методом, очевидно, предназначенным для переопределения подклассами. Этот метод представляет собой метод отложенного вызова, который выполняется, когда поток вызывает get() или set(Object) в первый раз и только один раз. Реализация по умолчанию в ThreadLocal просто возвращает null.
Случай: создайте три потока, каждый поток генерирует свой собственный независимый серийный номер.
class Res {
public static Integer count = 0;
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 0;
};
};
public Integer getNum() {
int count = threadLocal.get() + 1;
threadLocal.set(count);
return count;
}
}
public class Test006 extends Thread {
private Res res;
public Test006(Res res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + "," + res.getNum());
}
}
public static void main(String[] args) {
Res res = new Res();
Test006 t1 = new Test006(res);
Test006 t2 = new Test006(res);
t1.start();
t2.start();
}
}
Принцип реализации ThreadLoca, ThreadLoca через коллекцию карт, Map.put ("текущий поток", значение);
Многопоточность имеет три характеристики
что такое атомарность
То есть операция или несколько операций либо выполняются все и процесс выполнения не прерывается никакими факторами, либо ни одна из них не выполняется.
Очень классический пример — проблема с банковским переводом:
Например, перевод 1000 юаней со счета A на счет B должен включать две операции: вычитание 1000 юаней со счета A и добавление 1000 юаней на счет B. Эти две операции должны быть атомарными, чтобы гарантировать отсутствие непредвиденных проблем.
То же самое верно, когда мы оперируем данными, такими как i = i+1, что включает в себя чтение значения i, вычисление i и запись i. Эта строка кода не является атомарной в Java, поэтому многопоточная работа обязательно вызовет проблемы, поэтому нам также нужно использовать синхронизацию и блокировку, чтобы обеспечить эту функцию.
Атомарность на самом деле является частью обеспечения согласованности данных и безопасности потоков.
что такое видимость
Когда несколько потоков обращаются к одной и той же переменной, один поток изменяет значение переменной, а другие потоки сразу же видят измененное значение.
Если два потока находятся на разных процессорах, то значение i, измененное потоком 1, не было обновлено в основной памяти, а значение i снова используется потоком 2, тогда значение i должно быть таким же, как и раньше. , а поток 1 не видел изменения переменной потоком 1. Это проблема видимости.
что такое порядок
Порядок выполнения программы соответствует порядку выполнения кода. Вообще говоря, для повышения эффективности программы процессор может оптимизировать входной код, что не гарантирует, что последовательность выполнения каждого оператора в программе будет такой же, как последовательность в коде, но гарантирует, что выполняется окончательный результат выполнения программы и последовательность выполнения кода.Результаты непротиворечивы. следующее:
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
Тогда из-за переупорядочения он также может выполнять в порядке 2-1-3-4, 1-3-2-4
Но никогда не 2-1-4-3, потому что это нарушает зависимости.
Очевидно, что переупорядочивание не вызовет проблем при однопоточной работе, но не обязательно при многопоточной, поэтому мы должны учитывать эту проблему при программировании с несколькими потоками.
Модель памяти Java
Модель разделяемой памяти относится к модели памяти Java (сокращенно JMM),JMM определяет, что когда поток записывает в общую переменную, его может видеть другой поток.С точки зрения абстракции JMM определяет абстрактную связь между потоками и основной памятью:Общие переменные между потоками хранятся в основной памяти (mainmemory), каждый поток имеет частную локальную память (local memory), локальная память хранится в потоке для чтения/записи копии общей переменной.Локальная память является абстракцией JMM и на самом деле не существует. Он охватывает кеши, буферы записи, регистры и другие оптимизации оборудования и компилятора.
Из приведенного выше рисунка видно, что если поток A и поток B хотят взаимодействовать, они должны выполнить следующие два шага:- Сначала поток A сбрасывает обновленные общие переменные из локальной памяти A в основную память.
- Затем поток B переходит в основную память, чтобы прочитать общую переменную, которую ранее обновил поток A.
Два шага показаны на следующей диаграмме:
Как показано на рисунке выше, в локальной памяти A и B есть копии общей переменной x в основной памяти. Предположим, что изначально все значения x в этих трех памяти равны 0. Когда поток A выполняется, он временно сохраняет обновленное значение x (при условии, что значение равно 1) в своей собственной локальной памяти A. Когда потоку A и потоку B необходимо взаимодействовать, поток A сначала обновляет измененное значение x из своей локальной памяти в основную память, и в это время значение x в основной памяти становится равным 1. Затем поток B переходит в основную память для чтения обновленного значения x потока A, и значение x локальной памяти потока B также становится равным 1.В целом, эти два шага представляют собой поток A, отправляющий сообщение потоку B, и этот коммуникационный процесс должен проходить через основную память. JMM обеспечивает гарантии видимости памяти для Java-программистов, контролируя взаимодействие между основной памятью и локальной памятью каждого потока.
Суммировать:Что такое модель памяти Java. Модель памяти Java, или сокращенно jmm, определяет видимость потока для другого потока. Общие переменные хранятся в основной памяти, и каждый поток имеет свою собственную локальную память.Когда несколько потоков обращаются к данным одновременно, локальная память может не обновляться в основную память вовремя, поэтому возникают проблемы с безопасностью потоков.
Volatile
Видимость означает, что как только поток модифицирует изменяемую переменную, он гарантирует, что измененное значение будет немедленно обновлено в основной памяти.Когда другим потокам потребуется его прочитать, измененное значение может быть получено немедленно. Для ускорения работы программы на Java работа с некоторыми переменными обычно выполняется в регистре потока или в кэше процессора, после чего она будет синхронизирована с основной памятью, а переменная с модификатор volatile напрямую считывает и записывает основную память.
Volatile гарантирует своевременную видимость общих переменных между потоками, но не гарантирует атомарность.
class ThreadDemo004 extends Thread {
public boolean flag = true;
@Override
public void run() {
System.out.println("线程开始...");
while (flag) {
}
System.out.println("线程結束...");
}
public void setRuning(boolean flag) {
this.flag = flag;
}
}
public class Test0004 {
public static void main(String[] args) throws InterruptedException {
ThreadDemo004 threadDemo004 = new ThreadDemo004();
threadDemo004.start();
Thread.sleep(3000);
threadDemo004.setRuning(false);
System.out.println("flag已經改為false");
Thread.sleep(1000);
System.out.println("flag:" + threadDemo004.flag);
}
}
уже установил результат на fasle, почему? Он все еще работает.
Причина: Невидим между потоками, копия читается, а результат основной памяти не читается вовремя.
Решение Использование ключевого слова Volatile устранит видимость между потоками, заставив поток обращаться к «основной памяти» за значением каждый раз, когда он считывает значение.
Нестабильные функции
- Гарантировать видимость этой переменной всем потокам, "видимость" здесь, как упоминалось в начале этой статьи, когда поток модифицирует значение этой переменной, volatile гарантирует, что новое значение может быть немедленно синхронизировано с основной памятью, и каждое использование Непосредственно перед сбросом из основной памяти. Но обычные переменные этого сделать не могут, значение обычных переменных нужно передавать между потоками через основную память (подробности см. в: Модель памяти Java).
- Отключите оптимизацию переупорядочения инструкций. Для переменных с volatile-модификацией после присваивания выполняется еще одна операция "load addl $0x0, (%esp)", что эквивалентно барьеру памяти (следующие инструкции нельзя переупорядочить в позицию перед барьером памяти при переупорядочении инструкций ), когда к памяти обращается только один ЦП, барьер памяти не требуется; (что такое переупорядочивание инструкций: это означает, что ЦП использует метод, который позволяет отправлять несколько инструкций каждому соответствующему блоку схемы отдельно для обработки в порядке, не указанном по программе).
нестабильная производительность:
Потребление производительности при чтении volatile почти такое же, как у обычных переменных, но операция записи немного медленнее, потому что ей нужно вставить много инструкций барьера памяти в собственный код, чтобы гарантировать, что процессор не будет выполняться не по порядку.
Разница между изменчивым и синхронизированным
- Таким образом, мы видим, что хотя volatile имеет видимость, это не гарантирует атомарность.
- С точки зрения производительности, ключевое слово synchronized предотвращает одновременное выполнение фрагмента кода несколькими потоками, что повлияет на эффективность выполнения программы, а ключевое слово volatile в некоторых случаях имеет лучшую производительность, чем synchronized.
Однако следует отметить, что ключевое слово volatile не может заменить ключевое слово synchronized, поскольку ключевое слово volatile не может гарантировать атомарность операции.
изменение порядка
зависимость данных
Если две операции обращаются к одной и той же переменной, и одна из двух операций является операцией записи, между двумя операциями существует зависимость данных. Существует три типа зависимостей данных:
название | пример кода | инструкция |
---|---|---|
читать после записи | a = 1;b = a; | После записи переменной прочитайте местоположение. |
пиши после пиши | a = 1;a = 2; | После записи переменной напишите переменную снова. |
писать после прочтения | a = b;b = 1; | После чтения переменной запишите переменную. |
В приведенных выше трех случаях, если порядок выполнения двух операций изменен, результат выполнения программы будет изменен. Как упоминалось ранее, компиляторы и процессоры могут изменять порядок операций. Компилятор и процессор будут учитывать зависимости данных при переупорядочивании, а компилятор и процессор не изменят порядок выполнения двух операций, которые имеют зависимости данных. Обратите внимание, что упомянутые здесь зависимости данных относятся только к последовательностям инструкций, выполняемых в одном процессоре, и операциям, выполняемым в одном потоке, а зависимости данных между разными процессорами и между разными потоками не учитываются компилятором и процессором.
как-будто-серийная семантика
Смысл семантики s-if-serial в том, что результат выполнения (однопоточной) программы не может быть изменен, как бы он ни был переупорядочен (компиляторы и процессоры для улучшения параллелизма). Компиляторы, среды выполнения и процессоры должны подчиняться семантике как бы последовательной.
В соответствии с семантикой «как если бы» компиляторы и процессоры не переупорядочивают операции, которые имеют зависимости от данных, потому что такое переупорядочивание может изменить результат выполнения. Однако эти операции могут быть переупорядочены компилятором и процессором, если между ними нет зависимостей данных. Для иллюстрации взгляните на следующий пример кода для вычисления площади круга:
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
Зависимости данных трех вышеуказанных операций показаны на следующем рисунке:
Как показано на рисунке выше, существует зависимость данных между A и C, а также зависимость данных между B и C. Следовательно, C нельзя переупорядочить перед A и B в окончательной последовательности выполняемых инструкций (C помещается перед A и B, и результат программы будет изменен). Но между A и B нет зависимости данных, и компилятор и процессор могут изменить порядок выполнения между A и B. На следующем рисунке показаны две последовательности выполнения программы: Семантика "как-если-последовательно" защищает однопоточную программу, а компилятор, среда выполнения и процессор, соответствующие семантике "как-если-последовательно", вместе создают иллюзию для программиста, который пишет однопоточную программу: программа выполняется программа за программой по порядку. семантика as-if-serial освобождает однопоточных программистов от беспокойства о переупорядочении, мешающем им, или о проблемах с видимостью памяти.правила порядка программы
В соответствии с правилами последовательности программы «происходит-прежде» в приведенном выше примере кода для вычисления площади круга есть три отношения «произошло-прежде»:
- А случается раньше Б;
- B случается раньше C;
- Бывает - раньше С;
Третье отношение «происходит-прежде» здесь выводится из транзитивности «происходит-прежде». Здесь A происходит раньше B, но B может быть выполнено раньше A, когда оно действительно выполняется (см. измененный порядок выполнения выше). Как упоминалось в главе 1, если A происходит раньше B, JMM не требует, чтобы A выполнялось до B. JMM требует только, чтобы предыдущая операция (результат выполнения) была видна последней операции и чтобы первая операция была упорядочена перед второй операцией. Здесь результат выполнения операции A не обязательно должен быть виден операции B, а результат выполнения после переупорядочения операций A и B согласуется с результатом выполнения операций A и B в порядке «происходит до». В этом случае JMM будет считать такое изменение порядка не незаконным (не незаконным), а JMM разрешает такое изменение порядка. В компьютерах программная технология и аппаратная технология имеют общую цель: максимально развить степень параллелизма без изменения результата выполнения программы. Компиляторы и процессоры следуют этой цели, и из определения «случается-прежде» мы видим, что JMM также следует этой цели.
Влияние переупорядочения на многопоточность
/**
* 重排序
*/
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
System.out.println("writer");
}
public void reader() {
if (flag) { // 3
int i = a * a; // 4
System.out.println("i:" + i);
}
System.out.println("reader");
}
public static void main(String[] args) {
ReorderExample reorderExample = new ReorderExample();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
reorderExample.writer();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
reorderExample.reader();
}
});
t1.start();
t2.start();
}
}
Переменная flag — это флаг, указывающий, была ли записана переменная a. Здесь предполагается, что есть два потока A и B, A сначала выполняет метод write(), а затем поток B выполняет метод reader(). Когда поток B выполняет операцию 4, может ли он увидеть запись потока A в общую переменную a в операции 1? Ответ: не обязательно видимый. Поскольку операции 1 и 2 не имеют зависимостей по данным, компилятор и процессор могут переупорядочивать эти две операции, аналогично операции 3 и 4 не имеют зависимостей по данным, и компилятор и процессор также могут переупорядочивать эти две операции. Давайте сначала посмотрим, что может произойти, если переставить операции 1 и 2? См. временную диаграмму выполнения программы ниже:
Как показано на рисунке выше, операции 1 и 2 переупорядочены. Когда программа выполняется, поток A сначала записывает переменную флага flag, а затем поток B читает эту переменную. Поскольку условие оценивается как истинное, поток B будет читать переменную a. В этот момент переменная a вообще не была записана потоком A, а семантика многопоточных программ нарушается переупорядочением!※Примечание. В этой статье красная пунктирная стрелка используется для обозначения неправильной операции чтения, а зеленая пунктирная стрелка используется для обозначения правильной операции чтения.
Посмотрим, что произойдет, если переупорядочить операции 3 и 4 (при таком переупорядочении можно, кстати, объяснить зависимости управления). Ниже приведена диаграмма последовательности выполнения программы после переупорядочения операций 3 и 4:
В программе операция 3 и операция 4 имеют управляющие зависимости. Когда в коде есть управляющие зависимости, это влияет на параллелизм выполнения последовательности команд. С этой целью компиляторы и процессоры используют спекулятивное выполнение, чтобы преодолеть влияние зависимостей управления на параллелизм. Взяв в качестве примера выполнение процессором предположения, процессор, выполняющий поток B, может прочитать и вычислить a*a заранее, а затем временно сохранить результат вычисления в аппаратном кэше, называемом буфером переупорядочивания ROB. Когда условие следующей операции 3 признано истинным, результат вычисления записывается в переменную i.Из рисунка видно, что выполнение предположения существенно меняет порядок операций 3 и 4. Переупорядочивание здесь нарушает семантику многопоточных программ!
В однопоточной программе переупорядочивание операций с управляющими зависимостями не изменит результат выполнения (именно поэтому семантика «как если бы-последовательная» позволяет переупорядочивать операции, зависящие от управляющих воздействий); но в многопоточных программах переупорядочивание операций, имеющих управляющие зависимости, может измениться. результат выполнения программы.