Разница между volatile и синхронизированным

Java задняя часть JVM сервер

1. volatile-модифицированные переменные имеют видимость

113

Как видно из рисунка:

①Каждый поток имеет свое собственное пространство локальной памяти – пространство стека потока???Когда поток выполняется, он сначала считывает переменную из основной памяти в собственное пространство локальной памяти потока, а затем работает с переменной

②После завершения операции с переменной обновите переменную обратно в основную память в определенное время.

public class RunThread extends Thread {

 private boolean isRunning = true;

 public boolean isRunning() {
 return isRunning;
 }

 public void setRunning(boolean isRunning) {
 this.isRunning = isRunning;
 }

 @Override
 public void run() {
 System.out.println("进入到run方法中了");
 while (isRunning == true) {
 }
 System.out.println("线程执行完成了");
 }
}

public class Run {
 public static void main(String[] args) {
 try {
 RunThread thread = new RunThread();
 thread.start();
 Thread.sleep(1000);
 thread.setRunning(false);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
}

В строке 28 Run.java основной поток устанавливает для общей переменной в запущенном потоке RunThread значение false, тем самым завершая цикл while в строке 14 RunThread.java.

Если мы используем параметр JVM -server для выполнения программы, поток RunThread не завершится! Таким образом, существует бесконечный цикл! !

Анализ причин:

Теперь есть два потока, один основной поток, а другой RunThread. Они оба пытаются изменить переменную isRunning в третьей строке. Согласно модели памяти JVM, основной поток считывает isRunning в пространство памяти локального потока и после модификации обновляет его обратно в основную память.

Когда JVM настроен на запуск программы в режиме -server, поток всегда будет читать переменную isRunning в частном стеке. Поэтому поток RunThread не может прочитать переменную isRunning, измененную основным потоком.

В результате возникает бесконечный цикл, из-за которого RunThread не может завершить работу.Эта ситуация в «Эффективной JAVA» называется «активным сбоем».

Решение состоит в том, чтобы изменить его с помощью ключевого слова volatile в третьей строке кода. Здесь он заставляет поток извлекать изменчивую декорированную переменную из основной памяти.

2. volatile запрещает перестановку инструкций

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

Например, операция самоинкремента i++ переменной делится на три шага:

①Чтение значения переменной i из памяти

②Добавить 1 к значению i

③ Запишите значение после добавления 1 обратно в память.

Это показывает, что i++ не является атомарной операцией. Поскольку он разделен на три шага, возможно, что поток будет прерван при достижении второго шага, а это означает, что выполняются только два шага, а не все.

Относительно неатомарности volatile см. пример:

public class MyThread extends Thread {
 public volatile static int count;

 private static void addCount() {
 for (int i = 0; i < 100; i++) {
 count++;
 }
 System.out.println("count=" + count);
 }

 @Override
 public void run() {
 addCount();
 }
}

public class Run {
 public static void main(String[] args) {
 MyThread[] mythreadArray = new MyThread[100];
 for (int i = 0; i < 100; i++) {
 mythreadArray[i] = new MyThread();
 }

 for (int i = 0; i < 100; i++) {
 mythreadArray[i].start();
 }
 }
}

В строке 2 класса MyThread переменная count изменяется с помощью volatile

Строка 20 Run.java создает 100 потоков в цикле for, строка 25 запускает эти 100 потоков для выполнения addCount(), каждый поток выполняется 100 раз плюс 1

Ожидаемый правильный результат должен быть 100*100=10000, но на самом деле счет не достигает 10000

Причина в том, что volatile-модифицированные переменные не гарантируют атомарности операций над ними (автоинкремент). (Для операций автоинкремента вы можете использовать атомарный класс JAVA AutoicInteger для обеспечения атомарного автоинкремента)

Например, предположим, что i увеличивается до 5. Поток A считывает i из основной памяти со значением 5, сохраняет его в своем собственном пространстве потока и выполняет операцию увеличения со значением 6. В этот момент ЦП переключается на выполнение потока B и считывает значение переменной i из памяти ведущий-ведомый. Поскольку поток A не успел записать результат прибавления 1 обратно в основную память, поток B уже прочитал i из основной памяти, поэтому значение переменной i, считанное потоком B, по-прежнему равно 5.

Это эквивалентно чтению потоком B устаревших данных, что приводит к небезопасности потока.Эта ситуация называется «сбой безопасности» в Effective JAVA.

Таким образом, volatile само по себе не может гарантировать потокобезопасность. (атомарность)

3. synchronized

Synchronized может воздействовать на часть кода или метод, что может обеспечить как видимость, так и атомарность.

Видимость отражается в: synchronized или Lock могут гарантировать, что только один поток получает блокировку одновременно, а затем выполняет код синхронизации, а изменение переменной сбрасывается в основную память перед снятием блокировки.

Атомарность выражается в: либо не выполнять, либо выполнять до конца.

Если у вас все еще есть сомнения относительно приведенных выше результатов выполнения, не беспокойтесь, давайте сначала разберемся с принципом синхронизированного выполнения, а затем вернемся к вышеуказанным проблемам, чтобы было понятно с первого взгляда. Давайте сначала посмотрим, как Synchronized синхронизирует блоки кода, декомпилировав следующий код:

package com.paddx.test.concurrent;

public class SynchronizedDemo {
 public void method() {
 synchronized (this) {
 System.out.println("Method 1 start");
 }
 }
}

Результат декомпиляции:

820406-20160414215316020-1963237484

Что касается роли этих двух инструкций, то мы напрямую ссылаемся на описание в спецификации JVM:

монитор введите:

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• Если количество входов монитора, связанного с objectref, равно нулю, поток входит в монитор и устанавливает свой счетчик входов равным 1. Поток становится владельцем монитора.
• Если потоку уже принадлежит монитор, связанный с objectref, он повторно входит в монитор, увеличивая свой счетчик входов.
• Если монитор, связанный с objectref, уже принадлежит другому потоку, поток блокируется до тех пор, пока счетчик записей монитора не станет равным нулю, а затем снова пытается завладеть им.

Это предложение примерно означает:

Каждый объект имеет блокировку монитора (монитор). Когда монитор занят, он будет в заблокированном состоянии.Когда поток выполняет команду monitorenter, он пытается получить право собственности на монитор.Процесс выглядит следующим образом:

1. Если номер записи монитора равен 0, поток входит в монитор, а затем номер записи устанавливается равным 1, и поток становится владельцем монитора.

2. Если поток уже владеет монитором и просто повторно входит в него, количество входов в монитор увеличивается на 1.

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

Выход монитора:

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

Это предложение примерно означает:

Поток, выполняющий monitorexit, должен быть владельцем монитора, соответствующего объектной ссылке.

Когда инструкция выполняется, номер записи монитора уменьшается на 1. Если номер записи равен 0 после уменьшения на 1, поток выходит из монитора и больше не является владельцем монитора. Другие потоки, заблокированные этим монитором, могут попытаться завладеть этим монитором.

Благодаря этим двум описаниям мы должны четко видеть принцип реализации Synchronized. Базовая семантика Synchronized завершается через объект монитора. Фактически, такие методы, как ожидание/уведомление, также зависят от объекта монитора, поэтому только в синхронизации Wait/notify и другие методы могут быть вызваны только в блоке или методе .

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

Суммировать

1.volatile можно использовать только на уровне переменной, синхронизированный можно использовать на уровне переменной, метода и класса

2. volatile может обеспечить только видимость модификации переменных и не может гарантировать атомарность, синхронизированный может гарантировать видимость модификации и атомарность переменных

3.volatile не приведет к блокировке потока, синхронизация может привести к блокировке потока.

4. Переменные, помеченные как volatile, не будут оптимизированы компилятором, переменные, помеченные как synchronized, могут быть оптимизированы компилятором