предисловие
Звание помещика на самом деле имеет привкус смерти.Почему эти три вещи на самом деле можно написать в трех статьях, но домовладелец считает, что эти три вещи тесно связаны и должны быть в одном пункте знаний. Поймите эти вещи в одном исследовании. Чтобы лучше понять модель памяти Java, ключевое слово volatile и принцип HB.
Хозяин попытался рассказать об этих трех проблемах в сегодняшней статье и, наконец, пришел к выводу.
- Знание оборудования, которое необходимо рассмотреть, прежде чем говорить о знании параллелизма.
- Что, черт возьми, такое модель памяти Java?
- Что определяет модель памяти Java?
- Что такое принцип Happen-Before, полученный из модели памяти Java?
- Что такое volatile, представленное Happen-Before?
- Суммируйте все три.
1. Знание оборудования, которое необходимо изучить, прежде чем говорить о знании параллелизма.
Прежде всего, поскольку нам нужно понять параллелизм виртуальной машины Java, параллелизм физического оборудования очень похож на параллелизм виртуальных машин, и многие странные конструкции параллелизма виртуальных машин вызваны конструкцией физических машин.
Что такое параллелизм? Несколько процессоров работают одновременно. Но обратите внимание: одного ЦП недостаточно, ЦП может только вычислять данные, так откуда берутся данные?
Ответ: память. Данные поступают из памяти. Необходимо читать данные и сохранять результаты расчетов. Кто-то из студентов может сказать, а разве нет регистров и многоуровневых кэшей? Но это статическая оперативная память, которая слишком дорогая, SRAM использует в своей конструкции большое количество транзисторов, цена высока, и сделать большую емкость непросто, можно использовать только небольшую часть встроенного процессора. , становится кешем ЦП. Обычным использованием является динамическая оперативная память (Dynamic Random Access Memory). У Intel CPU FSB нужен доступ к памяти с северного моста, а у AMD северный мост не проектируется.Разница между ним и Intel в том, что память напрямую общается с CPU минуя северный мост, то есть управление памятью Компонент интегрирован в ЦП. Теоретически это может ускорить скорость передачи процессора и памяти.
Ну не важно какой ЦП, ему нужно читать данные из памяти, и у него есть свой кеш или регистр. Какая польза от кэша? Поскольку скорость ЦП очень высока, память вообще не может угнаться за ЦП, поэтому необходимо добавить слой кеша непосредственно в память и ЦП, чтобы они могли буферизовать данные ЦП: скопируйте данные, которые необходимо использовать в операции, в кеш, чтобы операцию можно было быстро выполнить, а затем синхронизируйте из кеша в память, когда операция будет завершена. Таким образом, процессору не нужно ждать медленных операций чтения и записи памяти.
Но это приводит к другой проблеме: когерентность кэша. Что это обозначает?
В мультипроцессорах каждый процессор имеет свой собственный кеш, и они совместно используют одну и ту же основную память (основную память).Когда вычислительные задачи нескольких процессоров включают одну и ту же область основной памяти, это может привести к тому, что соответствующие данные кеша будут несогласованными. Если это произойдет, чьи данные кеша используются при синхронизации с основной памятью?
В ранних процессорах проблема несогласованности кэша могла быть решена путем добавления блокировки LOCK# на шину. Поскольку связь между ЦП и другими компонентами осуществляется через шину, если шина заблокирована с помощью LOCK#, это означает, что другие ЦП заблокированы от доступа к другим компонентам (например, к памяти), так что только один ЦП может использовать эту шину. переменная память.
Чтобы решить проблему когерентности текущего ЦП, каждый ЦП должен следовать некоторым протоколам при доступе (чтении или записи) к кешу: MSI, MESI, MOSI, Synapse, Firefly, Dragon Protocol, все это протоколы когерентности кеша.
Итак, в это время вам нужно произнести существительное: модель памяти.
Что такое модель памяти?
Модель памяти можно понимать как абстракцию процесса доступа для чтения и записи к определенной памяти или кешу в рамках определенного протокола операции. ЦП разных архитектур имеют разные модели памяти, и виртуальная машина Java скрывает различия между моделями памяти ЦП, которая является моделью памяти Java.
Так как же выглядит структура модели памяти Java?
Ну, почти то же самое мы уже сказали о том, почему существует модель памяти, вообще говоря, потому, что многоуровневый кеш нескольких процессоров, обращающихся к одному и тому же модулю памяти, может привести к несогласованности данных. Следовательно, необходим протокол, позволяющий этим процессорам соответствовать этим протоколам для обеспечения согласованности данных при доступе к памяти.
еще есть вопрос. Выполнение конвейера ЦП и выполнение вне очереди
Предположим, у нас есть кусок кода:
int a = 1;
int b = 2;
int c = a + b;
Можем ли мы переместить приведенный выше код вне последовательности, и результат останется прежним? Да, нет ничего плохого в том, чтобы поменять местами первую и вторую строчки.
На самом деле ЦП иногда меняет порядок кода (исходя из того, что гарантируется результат) для оптимизации производительности.Технический термин называется переупорядочиванием. Почему изменение порядка оптимизирует производительность?
Это немного сложно, давайте поговорим об этом медленно.
Мы знаем, что выполнение инструкции можно разделить на множество шагов, проще говоря, на следующие шаги:
- Получить ЕСЛИ
- Идентификатор операнда декодирования и извлечения регистра
- Выполнить или вычислить эффективный адрес EX
- память возвращает MEM
- напиши в ВБ
Наши инструкции по сборке не могут быть выполнены за один шаг.При фактической работе в процессоре его также необходимо разделить на несколько шагов, которые будут выполняться последовательно.Аппаратное обеспечение, задействованное в каждом шаге, также может быть разным.Например, это будет используется при выборке инструкций.Регистры и память ПК, банк регистров команд используется при декодировании, АЛУ используется при выполнении, а банк регистров необходим при обратной записи.
То есть, поскольку каждый шаг можно было выполнять с использованием разного оборудования, инженеры ЦП изобрели конвейерную обработку для выполнения инструкций. Что это обозначает?
Если вам нужно помыть машину, то автомойка выполнит команду «помыть машину», однако автомойка будет работать отдельно, например, промывка, вспенивание, мойка, сушка, вощение и т. д. Эти действия можно выполнить с помощью разных сотрудников.Для этого одному сотруднику не нужно брать исполнение по очереди, а остальные сотрудники ждут там.Поэтому каждому сотруднику ставится задача, а после завершения выполнения она передается следующий работник, как конвейер на заводе.
Это то, что делает ЦП при выполнении инструкций.
Поскольку это конвейерное выполнение, конвейер нельзя прерывать, иначе прерывание в одном месте повлияет на эффективность выполнения всех нижестоящих компонентов, и производительность понесет большие потери.
Так что делать? Например, 1 промывка, 2 вспенивания, 3 очистки, 4 сушки и 5 вощения изначально выполнялись по порядку. Если воды в это время нет, то это повлияет на действия после смыва, но на самом деле мы можем сначала пустить смывную воду в воду, а положение вспенивателя изменить, так что мы первыми ударим по пене , и промывка В это время будет набрана вода.Когда пена первой машины закончится, промывка вернется и продолжит мчаться обратно, не влияя на работу. Затем последовательность становится такой:
1 дюжина пены, 2 ополаскивателя, 3 скраба, 4 сушки, 5 воска.
Но на работу это никак не влияет. Трубопровод тоже не сломался. Выполнение вне очереди в ЦП на самом деле похоже на это. Его конечной целью является снижение производительности процессора.
Что ж, мы почти рассмотрели необходимые для сегодняшней статьи знания об оборудовании. Подводя итог, есть в основном 2 пункта:
- Многоуровневый кеш ЦП должен взаимодействовать с протоколом когерентности кеша при доступе к основной памяти. Этот процесс может быть абстрагирован в модель памяти.
- ЦП будет конвейеризировать инструкции для повышения производительности и будет выполняться не по порядку, не загромождая исполнительную структуру одного ЦП.
Итак, следующий шаг — поговорить о модели памяти Java.
2. Что такое модель памяти Java?
Вспоминая вышесказанное, скажем, что такое модель памяти с аппаратного уровня?
Модель памяти можно понимать как абстракцию процесса доступа для чтения и записи к определенной памяти или кешу в рамках определенного протокола операции. Процессоры разных архитектур имеют разные модели памяти.
Являясь кросс-платформенным языком, Java должен скрывать различия между моделями памяти ЦП и создавать собственную модель памяти, которая является моделью памяти Java. Собственно, рут идет от памяти модели железа.
Еще посмотрите на эту картинку, модель памяти Java почти такая же, как модель памяти аппаратного обеспечения, каждый поток имеет свою собственную рабочую память, аналогичную кешу ЦП, а основная память java эквивалентна памяти оборудование.
Модель памяти Java также абстрагирует процесс доступа потоков к памяти.
JMM (модель памяти Java) указывает, что все переменные хранятся в основной памяти (это важно), включая поля экземпляра, статические поля и элементы, из которых состоят объекты данных, но не локальные переменные и параметры метода, поскольку последние являются приватными для потока. . не будет делиться. Естественно, о конкуренции речи не идет.
Что такое рабочая память? Каждый поток имеет свою рабочую память (это очень важно), в рабочей памяти потока хранятся переменные, используемые потоком, и копия копии основной памяти, а все операции (чтение и запись) над переменными выполняются потоком должны выполняться в оперативной памяти. Вместо чтения и записи переменных непосредственно в основную память. Различные потоки также не могут получить доступ к переменным в рабочей памяти друг друга. Передачу значений переменных между потоками нужно делать через основную память.
Подводя итог, можно сказать, что модель памяти Java определяет две важные вещи: 1. основную память и 2. рабочую память. Рабочая память каждого потока независима, и данные операций потока могут быть вычислены только в рабочей памяти, а затем сброшены в основную память. Это основная работа потоков, определенная моделью памяти Java.
3. Что определяет модель памяти Java?
Фактически вся модель памяти Java построена вокруг трех характеристик. Эти три характеристики лежат в основе всего параллелизма Java.
Атомарность, наглядность, упорядоченность.
атомарность
Что такое атомарность На самом деле эта атомарность в основном совпадает с определением атомарности при обработке транзакций. Относится к операции, которая является непрерывной и неделимой. Даже когда несколько потоков выполняются вместе, после запуска операции она не будет прервана другими потоками.
Мы можем грубо считать, что доступ к базовым типам данных является атомарным (однако, если вы вычисляете long и double на 32-битной виртуальной машине), потому что спецификация виртуальной машины Java, операции над long и double не содержат обязательного определения атомарности. , но настоятельно рекомендуется атомарный. Таким образом, большинство коммерческих виртуальных машин в основном достигают атомарности.
Если пользователю необходимо работать с более широким диапазоном для обеспечения атомарности, то модель памяти Java обеспечивает операции блокировки и разблокировки (которые являются двумя из 8 операций с памятью) для удовлетворения этой потребности, но не предоставляет это программисту. , предоставляя более абстрактные команды monitorenter и monitorexit с двумя инструкциями байт-кода, то есть с ключевым словом synchronized. Таким образом, операции между синхронизированными блоками являются атомарными.
Видимость
Видимость означает, что, когда поток изменяет значение общей переменной, другие потоки могут немедленно узнать об этой модификации.Модель памяти Java синхронизирует новое значение с основной памятью после изменения переменной и обновляет переменную из основной памяти перед изменением. переменная имеет значение read.value, которая полагается на оперативную память как на средство доставки для реализации этой видимости. Это справедливо как для обычных, так и для изменчивых переменных. Разница между ними заключается в том, что специальные правила volatile гарантируют, что новое значение может быть немедленно синхронизировано с основной памятью, и может обновляться из основной памяти перед каждым использованием, поэтому можно сказать, что volatile гарантирует видимость переменных при многопоточных операциях, а обычные переменные этого не гарантируют.
В дополнение к volatile, синхронизированный и окончательный также обеспечивают видимость. Видимость синхронизированных блоков определяетсяПеред выполнением операции разблокировки переменной переменная должна быть синхронизирована обратно в основную память (выполнение операции сохранения, записи)..
Заказ
О проблеме упорядоченности мы упоминали, когда говорили об оборудовании вверху, ЦП будет корректировать порядок инструкций, и та же виртуальная машина Java также будет корректировать порядок байт-кодов, но эта корректировка не ощутима в отдельном потоке, За исключением многопоточных программ, эта настройка может привести к неожиданным ошибкам.
Java предложила два ключевых слова для обеспечения упорядочения операций между несколькими потоками.Ключевое слово volatile само по себе содержит семантику запрета переупорядочивания, тогда как synchronized определяется как «переменная позволяет только одному потоку выполнять правило« блокировки операции ». Это правило определяет, что два синхронизированных блока одного и того же замка могут входить только последовательно.
Итак, мы представили три основные характеристики JMM. Я не знаю, обнаружили ли вы, что volatile гарантирует видимость и упорядоченность, а synchronized гарантирует все три функции, что называется панацеей. И синхронизированный прост в использовании. Тем не менее, по-прежнему опасайтесь его влияния на производительность.
4. Что такое принцип «случается до того», как он вытекает из модели памяти Java?
Говоря об упорядочении, обратите внимание, что мы сказали, что упорядочение может быть достигнуто с помощью volatile и synchronized, но мы не можем полагаться на эти два ключевых слова для всего нашего кода. На самом деле в языке Java есть положения о переупорядочивании или упорядочивании, и эти положения нельзя нарушать при оптимизации виртуальной машины.
- Принцип порядка программы: внутри потока, в соответствии с порядком кода программы, операции, написанные впереди, выполняются раньше, чем операции, написанные сзади.
- Правило изменчивости: запись изменчивой переменной происходит до чтения, что обеспечивает видимость изменчивой переменной.
- Правила блокировки: разблокировка (unlock) должна произойти до последующей блокировки (lock).
- Транзитивность: А предшествует В, В предшествует С, тогда А должно предшествовать С.
- Метод запуска потока предшествует каждому из его действий.
- Все операции потока предшествуют завершению потока.
- Прерывание потока (interrupt()) предшествует прерванному коду.
- Конструктор объекта, заканчивающийся перед методом finalize.
5. Что такое volatile, представленное Happen-Before?
Мы сказали много ключевого слова volatile в начале. Видно, что это ключевое слово очень важно, но кажется, что оно используется чаще, чем синхронизированный Гораздо меньше, что мы знаем, что может сделать это ключевое слово?
Volatile может обеспечить видимость потоков и порядок потоков. Но атомарность не может быть достигнута.
Давайте просто напишем кусок кода!
package cn.think.in.java.two;
/**
* volatile 不能保证原子性,只能遵守 hp 原则 保证单线程的有序性和可见性。
*/
public class MultitudeTest {
static volatile int i = 0;
static class PlusTask implements Runnable {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
// plusI();
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int j = 0; j < 10; j++) {
threads[j] = new Thread(new PlusTask());
threads[j].start();
}
for (int j = 0; j < 10; j++) {
threads[j].join();
}
System.out.println(i);
}
// static synchronized void plusI() {
// i++;
// }
}
Мы запускаем 10 потоков к ++ каждой переменной int.Обратите внимание, что символ ++ не является атомарным. Затем основной поток ожидает этих 10 потоков и печатает значение int после завершения выполнения. Вы обнаружите, что как бы вы его ни запускали, оно никогда не достигнет 10000, потому что оно не атомарно. Как понять это?
i++ равно i = i + 1;
Виртуальная машина сначала считывает значение i, а затем добавляет к i 1. Обратите внимание, что volatile гарантирует, что значение, прочитанное потоком, является последним.Когда поток считывает i, значение действительно является последним, но есть все 10 тредов читают, читают последний и одновременно добавляют 1, эти операции не нарушают определения volatile. В итоге что-то пошло не так, так сказать, неправильно использовали.
Арендодатель также добавил в тестовый код метод синхронизации, который может гарантировать атомарность. Когда цикл for выполняет не i++, а метод plusI, то результат будет точным.
Итак, когда использовать volatile?
Результат операции не зависит от текущего значения переменной или гарантирует, что только один поток изменяет значение переменной.В случае с нашей программой результат операции зависит от текущего значения i, и если бы его изменить на атомарную операцию: i = j, то результатом было бы правильное 9999.
Например, следующая программа является примером использования volatile:
package cn.think.in.java.two;
/**
* java 内存模型:
* 单线程下会重排序。
* 下面这段程序再 -server 模式下会优化代码(重排序),导致永远死循环。
*/
public class JMMDemo {
// static boolean ready;
static volatile boolean ready;
static int num;
static class ReaderThread extends Thread {
public void run() {
while (!ready) {
}
System.out.println(num);
}
}
public static void main(String[] args) throws InterruptedException {
new ReaderThread().start();
Thread.sleep(1000);
num = 32;
ready = true;
Thread.sleep(1000);
Thread.yield();
}
}
Эта программа очень интересная, мы используем volatile переменные для управления потоком, окончательный правильный результат 32, но обратите внимание, что если вы не используете ключевое слово volatile, а виртуальная машина запущена с параметром -server, эта программа будет всегда будет не закончится, потому что он будет JIT-оптимизирован, и другой поток никогда не сможет увидеть изменение переменной (JIT игнорирует код, который считает недопустимым). Конечно, когда вы переходите на volatile, проблем нет.
Из приведенного выше кода мы знаем, что volatile не гарантирует атомарности, но может гарантировать порядок и видимость. Так как же это достигается?
Как обеспечить порядок? На самом деле, в ассемблерном коде до и после манипулирования ключевой переменной volatile будет префикс блокировки, согласноИнтел IA32 руководство, функция блокировки состоит в том, чтобы кэш ЦП записывался в память, и действие записи также приводит к тому, что другие ЦП или другие ядра делают свой кэш недействительным, и другим ЦП необходимо повторно получить кэш. Это обеспечивает видимость. Видно, что нижний слой по-прежнему является инструкцией используемого процессора.
Как добиться порядка? Это также инструкция блокировки, которая также эквивалентна барьеру памяти (大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障
), что означает, что следующие инструкции нельзя переупорядочить в положение перед барьером памяти во время переупорядочивания. Когда к памяти обращается только один ЦП, барьер памяти не требуется; но если два или более ЦП обращаются к одной и той же памяти, и один из них наблюдает за другим, для обеспечения этого требуется барьер памяти.
Поэтому, пожалуйста, не используйте изменчивые переменные произвольно, это приведет к тому, что JIT не сможет оптимизировать код и вставит много инструкций барьера памяти, что снизит производительность.
6. Резюме
Во-первых, JMM абстрагирует аппаратную модель памяти (использование многоуровневых кэшей приводит к протоколу когерентности кэшей), что скрывает различия между различными процессорами и операционными системами.
Модель памяти Java относится к процессу доступа к памяти по определенному протоколу. То есть рабочая память потока и непосредственная последовательность операций основной памяти.
JMM в основном устанавливает спецификации, связанные с атомарностью, видимостью и упорядоченностью.
синхронизированный может выполнять эти 3 функции, в то время как volatile может обеспечивать только видимость и упорядоченность. final также может быть видимостью реализации.
Принцип Happen-Before определяет, что не может быть изменено виртуальной машиной, включая правила для блокировок и правила для чтения и записи volatile-переменных.
А volatile, как мы уже говорили, не может гарантировать атомарность, поэтому нужно быть внимательным при его использовании. Базовой реализацией volatile является инструкция блокировки ЦП, которая обеспечивает видимость, очищая кэши остальной части ЦП, и обеспечивает упорядоченность через ограждение памяти.
В целом можно сказать, что эти три понятия тесно связаны. Они зависят друг от друга. Так что арендодатель поместил это в статью для написания, но это может привести к некоторым упущениям, но это не мешает нам понять всю концепцию. Можно сказать, что JMM — это основа всего параллельного программирования, и если вы не понимаете JMM, эффективный параллелизм невозможен.
Конечно, наша статья все еще недостаточно низкоуровневая, и не анализировала внутреннюю реализацию JVM, сегодня уже очень поздно, если у нас есть возможность, давайте введем исходный код JVM, чтобы увидеть их базовую реализацию.
удачи! ! ! !