Java -одновременное программирование серии [незаконченных]:
• Программирование параллелизма Java: основная теория
• Параллельное программирование Java: синхронизированные и его принципы реализации
• Параллельное программирование Java: синхронизированная базовая оптимизация (легкий замок, смещенная блокировка)
• Параллельное программирование Java: Сотрудничество между потоками (WAIT/NOTIFY/SHEEP/GIRNT/JOIN)
• Параллельное программирование Java: использование летучих и его принципов.
1. Роль летучих
В статье «Программирование параллелизма Java: основная теория» мы упомянули проблемы видимости, упорядочения и атомальности. Обычно мы можем решить эти проблемы с помощью синхронизированного ключевого слова. Однако, если у вас есть понимание принципа синхронизации, вы должны знать, что синхронизированный является относительно тяжелым весом и оказывает относительно большое влияние на производительность системы. Поэтому, если есть другие решения, мы обычно избегаем использования синхронизации для решения проблемы. Ключевое слово «летучих» - это еще одно решение, предоставленное на Java для решения проблем видимости и упорядочения. Что касается атомности, то также является то, что каждый склонен к недопониманию: одна операция чтения/записи летучих переменных может обеспечить атомичность, такую как переменные длинного и двойного типа, но она не может гарантировать атомичность операций I ++, потому что, по сути, I ++ выполняет операции и записывает дважды.
2. Использование летучих
Что касается использования летучих, мы можем использовать несколько примеров, чтобы проиллюстрировать его использование и сценарии.
1. Предотвратить переупорядочение
Давайте проанализируем проблему повторного порядка из одного из самых классических примеров. Каждый должен быть знаком с реализацией модели Singleton, и в одновременной среде мы обычно можем использовать метод двойной проверки блокировки (DCL) для ее реализации. Исходный код заключается в следующем:
пакет com.paddx.test.concurrent; открытый класс Singleton {public static volatile Singleton Singleton; / *** Конструктор является частным, запрещает внешнюю экземпляр*/ private singleton () {}; public static singleton getInstance () {if (singleton == null) {synchronized (singleton) {if (singleton == null) {singleton = new Singleton (); }} вернуть Singleton; }}Теперь давайте проанализируем, почему нам нужно добавить нестабильное ключевое слово между переменной singleton. Чтобы понять эту проблему, вы должны сначала понять процесс построения объекта. Сменьшаясь объект может быть фактически разделен на три шага:
(1) Выделите пространство памяти.
(2) Инициализировать объект.
(3) Назначьте адрес пространства памяти соответствующей ссылке.
Однако, поскольку операционная система может переупорядочить инструкции, приведенный выше процесс также может стать следующим процессом:
(1) Выделите пространство памяти.
(2) Назначьте адрес пространства памяти соответствующей ссылке.
(3) Инициализировать объект
Если этот процесс является процессом, ненициализированная ссылка на объект может быть выявлена в многопоточной среде, что приведет к непредсказуемым результатам. Поэтому, чтобы предотвратить повторное заказа этого процесса, нам необходимо установить переменную на переменную летучих типов.
2. Достигнуть видимость
Проблема видимости в основном относится к одному потоку, изменяющему значение общей переменной переменной, в то время как другой поток не может его увидеть. Основная причина проблемы видимости заключается в том, что в каждом потоке есть своя область кеша - рабочая память потока. Волатильное ключевое слово может эффективно решить эту проблему. Давайте посмотрим на следующие примеры, чтобы узнать ее функцию:
пакет com.paddx.test.concurrent; открытый класс volatiletest {int a = 1; int b = 2; public void change () {a = 3; b = a; } public void print () {System.out.println ("b ="+b+"; a ="+a); } public static void main (string [] args) {while (true) {final volatileTest test = new volatileTest (); New Thread (new Runnable () {@Override public void run () {try {thread.sleep (10);} catch (прерывание Exception e) {e.printStackTrace ();} test.Change ();}}). start (); New Thread (new Runnable () {@Override public void run () {try {thread.sleep (10);} catch (прерывание Exception e) {e.printStackTrace ();} test.print ();}}). start (); }}}Интуитивно говоря, есть только два возможных результата для этого кода: b = 3; a = 3 или b = 2; a = 1. Тем не менее, запустив приведенный выше код (возможно, это займет немного больше времени), вы обнаружите, что в дополнение к двум предыдущим результатам также есть третий результат:
...... b = 2; a = 1b = 2; a = 1b = 3; a = 3b = 3; a = 3b = 3; a = 1b = 3; a = 3b = 2; a = 1b = 3; a = 3b = 3; a = 3b = 3; a = 3 ...
Почему появляется результат, такой как b = 3; a = 1? При нормальных обстоятельствах, если вы сначала выполняете метод изменения, а затем выполняете метод печати, результат вывода должен быть b = 3; a = 3. Напротив, если вы сначала выполняете метод печати, а затем выполните метод изменения, результат должен быть b = 2; a = 1. Так как же результат b = 3; a = 1 выходит? Причина в том, что первый поток изменяет значение a = 3, но невидим для второго потока, поэтому этот результат происходит. Если как A, так и B изменяются на переменные летучего типа и выполнены, результат B = 3; A = 1 никогда не появится снова.
3. Обеспечить атомность
Проблема атомичности была объяснена выше. Волатильный может гарантировать только атомность для единого чтения/записи. Эта проблема может быть описана в JLS:
17.7 Неатомическая обработка двойной и длительной для целей модели памяти языка программирования Java, одна запись в нелетую длительную или двойную ценность рассматривается как два отдельных записи: по одному по каждую 32-разрядную половину. Это может привести к ситуации, когда нить видит первые 32 бита 64-битного значения из одной записи, а вторые 32 бита от другой записи. Написаны и чтения летучих длинных и двойных значений всегда являются атомными. Записывает и читает ссылки, всегда атомны, независимо от того, реализованы ли они как 32-битные или 64-битные значения. Некоторые реализации могут быть удобным разделить одно действие записи на 64-разрядное длительное или двойное значение на два действия записи на соседние 32-разрядные значения. Для эффективности это поведение зависит от реализации; Реализация виртуальной машины Java может свободно выполнять записи до длинных и двойных значений атомно или в двух частях. Реализации виртуальной машины Java предлагается избегать разделения 64-битных значений, где это возможно. Программистам рекомендуется объявлять общие 64-разрядные значения как летучие или правильно синхронизировать свои программы, чтобы избежать возможных композиций.
Содержание этого отрывка примерно похожи на то, что я описал ранее. Поскольку операции двух типов данных длинных и двойных могут быть разделены на две части: высокие 32 бита и низкие 32 бита, обычные длинные или двойные типы могут не быть атомными. Поэтому каждому рекомендуется установить общие длинные и двойные переменные на летучие типы, что может гарантировать, что операции с одним чтением/записи длинных и двойных в любом случае являются атомальными.
Существует проблема, что нестабильные переменные гарантируют атомность, которая легко неправильно понята. Теперь мы продемонстрируем эту проблему с помощью следующей программы:
пакет com.paddx.test.concurrent; public class volatiletest01 {volatile int i; public void addi () {i ++; } public static void main (string [] args) бросает прерывание {final volatileTest01 test01 = new volatileTest01 (); for (int n = 0; n <1000; n ++) {new Thread (new Runnable () {@Override public void run () {try {thread.sleep (10);} catch (прерывание Exception e) {e.printstackTrace ();} test01.addi ();}}). start (); } Thread.sleep (10000); // ждать 10 секунд, чтобы гарантировать, что вышеупомянутое выполнение программы выполнено System.out.println (test01.i); }}Вы можете ошибочно поверить, что после добавления ключевого слова волатило к переменной I, эта программа безопасна для потока. Вы можете попробовать запустить вышеуказанную программу. Вот результаты моего местного пробега:
Может быть, все получают результаты по -разному. Тем не менее, следует видеть, что летучие не может гарантировать атомность (в противном случае результат должен быть 1000). Причина также очень проста. I ++ на самом деле является композитной операцией, включая три шага:
(1) Прочитайте значение i.
(2) Добавить 1 в i.
(3) Напишите значение I обратно в память.
Нет никакой гарантии, что эти три операции являются атомными. Мы можем обеспечить атомичность операций +1 через Atomicinteger или синхронизирован.
ПРИМЕЧАНИЕ. Метод Thread.sleep () был выполнен во многих местах в вышеупомянутых разделах кода с целью увеличения вероятности проблем с параллелизмом и не имеет другого эффекта.
3. Принцип нестабильного
Через приведенные выше примеры мы должны в основном знать, что такое волатильное и как его использовать. Теперь давайте посмотрим на то, как реализован основной слой волатильного.
1. Реализация видимости:
Как упоминалось в предыдущей статье, сам поток напрямую не взаимодействует с основными данными памяти, а завершает соответствующие операции через рабочую память потока. Это также важная причина, по которой данные между потоками невидимы. Поэтому, чтобы достичь видимости летучих переменных, вы можете начать непосредственно с этого аспекта. Существует два основных различия между операциями написания на летучие переменные и обычные переменные:
(1) При изменении летучей переменной измененное значение будет вынуждено обновить основную память.
(2) Изменение летучей переменной приведет к сбою соответствующих значений переменной в рабочей памяти других потоков. Поэтому при снова прочтении значения этой переменной вам необходимо снова прочитать значение в основной памяти.
Благодаря этим двум операциям проблема видимости летучих переменных может быть решена.
2. Указывающая реализация:
Прежде чем объяснить эту проблему, давайте сначала поймем правила происшествия в Java. Определение случая, прежде чем в JSR 133 заключается в следующем:
Два действия могут быть заказаны по отношению к происхождению.
С точки зрения непрофессионала, если это произойдет до B, любые операции A Do Do Visible B. (Каждый должен помнить об этом, потому что слово происходит, прежде чем легко неправильно понято, как и раньше и после времени). Давайте посмотрим на то, что происходит,-правила, правила определены в JSR 133:
• Каждое действие в потоке происходит до каждого последующего действия в этом потоке. • Разблокировка на мониторе происходит до каждой последующей блокировки этого монитора. • Написать в летучих поле происходит до каждого последующего чтения этого летучего. • Призыв к запуску () в потоке произойдет до любых действий в запущенном потоке. • Все действия в потоке происходят до того, как любой другой поток успешно возвращается из join () в этом потоке. • Если действие A происходит до действия B, а B происходит до действия C, то A происходит до c.
Перевод как:
• Предыдущая операция произошла в той же потоке. (то есть, в одном потоке, законно выполнять в кодовом порядке. Однако компилятор и процессор могут переупорядочить, не влияя на результаты выполнения в одной резьбовой среде. Другими словами, это то, что правила не могут гарантировать переупорядочение компиляции и переупорядочение инструкций).
• Разблокируйте работу на мониторе, прежде чем его последующая операция блокировки. (Синхронизированные правила)
• Операция записи на нестабильную переменную, прежде чем последующие операции чтения. (летучие правила)
• Метод start () потока происходит, прежде чем все последующие операции потока. (Правило начала потока)
• Все операции потока происходят, прежде чем другие потоки вызывают в этом потоке и возвращают успешную работу.
• Если это произойдет, потому что B, B, прежде чем C, то случайно C (переход).
Здесь мы в основном рассмотрим третье правило: правила для обеспечения упорядочения летучих переменных. В статье «Программирование параллелизма Java: основная теория» упоминается, что переупорядочение делится на переупорядочение компилятора и переупорядочение процессора. Чтобы реализовать семантику летучих памяти, JMM ограничивает переупорядочение этих двух типов летучих переменных. Ниже приведена таблица правил повторного порядка, указанная JMM для летучих переменных:
| Может переупорядочить | 2 -я операция | |||
| 1 -я операция | Нормальная нагрузка Нормальный магазин | Нестабильная нагрузка | Нестабильный магазин | |
| Нормальная нагрузка Нормальный магазин | Нет | |||
| Нестабильная нагрузка | Нет | Нет | Нет | |
| Нестабильный магазин | Нет | Нет | ||
3. Барьер памяти
Чтобы реализовать нестабильную видимость и случиться с семантикой. Основной JVM делается через то, что называется «барьером памяти». Барьер памяти, также известный как забор памяти, представляет собой набор инструкций процессора, используемых для реализации последовательных ограничений на операции памяти. Вот барьер памяти, необходимый для выполнения вышеуказанных правил:
| Требуются барьеры | 2 -я операция | |||
| 1 -я операция | Нормальная нагрузка | Нормальный магазин | Нестабильная нагрузка | Нестабильный магазин |
| Нормальная нагрузка | Loadstore | |||
| Нормальный магазин | Storestore | |||
| Нестабильная нагрузка | Загрузка | Loadstore | Загрузка | Loadstore |
| Нестабильный магазин | StoreLoad | Storestore | ||
(1) Барьер загрузки
Заказ на выполнение: загрузка1 -> загрузку -> load2
Убедитесь, что нагрузка2 и последующие инструкции по загрузке могут получить доступ к данным, загруженным Load1, перед загрузкой данных.
(2) Барьер Storestor
Заказ на выполнение: Store1 -> Storestore -> Store2
Убедитесь, что данные операции Store1 видны для других процессоров перед Store2 и последующими инструкциями хранилища.
(3) Барьер Loadstore
Заказ на выполнение: load1―> loadstore -> store2
Убедитесь, что до выполнения инструкций Store2 и последующих хранилищ можно получить доступ, загруженные Load1.
(4) Барьер из StoreLoad
Заказ на выполнение: Store1 -> StoreLoad -> Load2
Убедитесь, что перед считыванием Load2 и последующими инструкциями по загрузке данных считываются данные Store1 для других процессоров.
Наконец, я могу использовать пример, чтобы проиллюстрировать, как вставлен барьер памяти в JVM:
пакет com.paddx.test.concurrent; открытый класс MemoryBarrier {int a, b; volatile int v, u; void f () {int i, j; i = a; J = B; i = V; // загрузка загрузки j = u; // Loadstore A = I; b = j; // Storestore V = I; // Storestore U = J; // StorEload I = U; // загрузка // Loadstore J = B; a = i; }}4. Резюме
В целом, понимание нестабильного все еще относительно сложно. Если вы не понимаете это особенно хорошо, вам не нужно торопиться. Требуется процесс, чтобы полностью понять это. Вы также увидите сценарии использования волатильных много раз в последующих статьях. Здесь у меня есть базовое понимание базовых знаний летучих и оригинальных. Вообще говоря, нестабильная оптимизация в одновременном программировании, которая может заменить синхронизированный в некоторых сценариях. Тем не менее, нестабильный не может полностью заменить положение синхронизации. Только в некоторых специальных сценариях можно применять летучие. В общем, следующие два условия должны быть выполнены одновременно, чтобы обеспечить безопасность потока в одновременной среде:
(1) Операция записи по переменным не зависит от текущего значения.
(2) Эта переменная не включена в инвариант с другими переменными.
Приведенная выше статья о параллельном программировании Java: использование летучих и ее принципиально -анализ - это все контент, которым я делюсь с вами. Я надеюсь, что вы можете дать вам ссылку, и я надеюсь, что вы сможете поддержать Wulin.com больше.