В последнем разделе я ударил одновременного хулигана со всеми вамивидимость, в этом разделе мы продолжаем крестовый поход против атомарности, одного из трех зол.
порядок, атомарность
Свойство, состоящее в том, что одна или несколько операций не прерываются во время выполнения ЦП, называется атомарностью.
Я понимаю, что операцию нельзя разделить, т.е.атомарность. В контексте параллельного программирования смысл атомарности в том, что пока поток начинает выполнять эту серию операций, либо все они выполняются, либо все не выполняются, и половина выполнения не допускается.
Давайте попробуем сравнить два аспекта транзакций базы данных и параллельного программирования:
1. В базе
Концепция атомарности такова: транзакция рассматривается как неделимое целое, и содержащиеся в ней операции выполняются либо все, либо ни одна из них. И если при выполнении транзакции возникает ошибка, то происходит откат к состоянию до начала транзакции, как если бы транзакция не выполнялась. (То есть: транзакция либо выполняется, либо ни одна из них не выполняется)
2. В параллельном программировании
Понятие атомарности выглядит так:
- Первое понимание: во время выполнения потока или процесса не происходит переключения контекста.
- Переключение контекста: относится к переключению ЦП с одного процесса/потока на другой процесс/поток (предпосылкой переключения является получение права на использование ЦП).
- Второе понимание: мы называем особенностью, при которой одна или несколько операций (неделимое целое) в потоке не прерываются во время выполнения ЦП, называемой атомарностью. (В процессе выполнения, как только произойдет прерывание, произойдет переключение контекста)
Как видно из описания атомарности выше, концепция атомарности между параллельным программированием и базами данных несколько похожа:Все подчеркивают, что атомарную операцию нельзя прервать!!
Неатомарная операция представлена вот такой картинкой:
Поток A выполняется какое-то время (еще не завершено), а затем назначает ЦП потоку B для выполнения. В операционной системе существует множество таких операций, жертвующих чрезвычайно коротким временем переключения потоков для улучшения использования ЦП, тем самым улучшая общую производительность системы; эта операция операционной системы называется переключением «кванта времени».
1. Причина проблемы атомарности
С помощью концепции атомарности, описанной во введении, мы пришли к выводу, что причиной проблемы атомарности общих переменных между потоками является переключение контекста.
Далее, давайте воспроизведем проблему атомарности на примере.
Сначала определите класс сущности банковского счета:
@Data
@AllArgsConstructor
public static class BankAccount {
private long balance;
public long deposit(long amount){
balance = balance + amount;
return balance;
}
}
Затем откройте несколько потоков для выполнения депозитных операций на этом общем банковском счете, каждый из которых вносит 1 юань:
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.ArrayList;
/**
* @author :mmzsblog
* @description:并发中的原子性问题
* @date :2020/2/25 14:05
*/
public class AtomicDemo {
public static final int THREAD_COUNT = 100;
static BankAccount depositAccount = new BankAccount(0);
public static void main(String[] args) throws Exception {
ArrayList<Thread> threads = new ArrayList<>();
for (int i = 0; i < THREAD_COUNT; i++) {
Thread thread = new DepositThread();
thread.start();
threads.add(thread);
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Now the balance is " + depositAccount.getBalance() + "元");
}
static class DepositThread extends Thread {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
depositAccount.deposit(1); // 每次存款1元
}
}
}
}
Запустите приведенную выше программу несколько раз, результаты каждый раз будут почти разными, и иногда мы можем получить ожидаемые результаты.100*1*1000=100000元
, ниже приведены результаты нескольких прогонов, которые я перечислил:
Причина описанной выше ситуации заключается в том, что
balance = balance + amount;
Этот код не является атомарной операцией, где баланс является общей переменной. Может прерываться в многопоточной среде. Таким образом, проблема атомарности оказывается обнаженной. как показано на рисунке:
Конечно, если balance — локальная переменная, то проблем не будет даже в случае многопоточности (но этот разделяемый банковский счет не относится к локальным переменным, иначе это не разделяемая переменная, ха-ха, равносильно бреду) , потому что локальные переменные являются частными для текущего потока. Точно так же, как переменная j в цикле for на рисунке.
Однако, даже если это общая переменная, я никогда не позволю такой проблеме возникнуть, поэтому нам нужно решить ее, а затем глубже понять проблему атомарности в параллельном программировании.
2. Решить проблему атомарности, вызванную переключением контекста
2.1 Использование локальных переменных
Область действия локальных переменных находится внутри метода, то есть, когда метод выполняется, локальные переменные отбрасываются, а локальные переменные и метод живут и умирают вместе. Фрейм стека вызовов также живет и умирает вместе с методом, поэтому вполне разумно размещать локальные переменные в стеке вызовов. На самом деле локальные переменные действительно помещаются в стек вызовов.
Именно потому, что каждый поток имеет свой собственный стек вызовов, а локальные переменные хранятся в соответствующем стеке вызовов потока и не будут использоваться совместно, поэтому, естественно, проблемы параллелизма не возникает. Подводя итог: без обмена ничего не может пойти не так.
Но если здесь использовать локальные переменные, каждый из 100 потоков сэкономит 1000 юаней, и в итоге все они будут начинаться с 0, а не накапливаться, и первоначальный результат, который вы хотите отобразить, будет утерян. Так что этот метод невозможен.
Точно так же, как использование одного потока здесь также может гарантировать атомарность, потому что это не подходит для текущего сценария, это не решает проблему.
2.2. Автономная атомная гарантия
В Java чтение и присвоение переменных примитивных типов данных являются атомарными операциями.
Например, следующие строки кода:
// 原子性
a = true;
// 原子性
a = 5;
// 非原子性,分两步完成:
// 第1步读取b的值
// 第2步将b赋值给a
a = b;
// 非原子性,分三步完成:
// 第1步读取b的值
// 第2步将b值加2
// 第3步将结果赋值给a
a = b + 2;
// 非原子性,分三步完成:
// 第1步读取a的值
// 第2步将a值加1
// 第3步将结果赋值给a
a ++;
2.3, синхронизированный
Определенно невозможно сделать весь Java-код атомарным, а то, что компьютер может обработать за один раз, всегда ограничено. Поэтому, когда атомарность не может быть достигнута, мы должны использовать стратегию, чтобы процесс выглядел атомарным. Так там синхронизировано.
Synchronized может гарантировать не только видимость операции, но и атомарность результата операции.
внутри экземпляра объекта,synchronized aMethod(){}
Синхронизированный метод, предотвращающий одновременный доступ нескольких потоков к этому объекту.
Если у объекта есть несколько синхронизированных методов, пока один поток обращается к одному из синхронизированных методов, другие потоки не могут одновременно обращаться ни к одному синхронизированному методу в объекте.
Поэтому здесь нам нужно только установить синхронизированный метод депозита, чтобы обеспечить атомарность.
private volatile long balance;
public synchronized long deposit(long amount){
balance = balance + amount; //1
return balance;
}
После добавления синхронизации другие потоки не могут выполнять код, измененный с помощью synchronized, пока поток не завершит выполнение метода синхронизированного депозита. Таким образом, даже если выполнение строки кода 1 будет прервано, другие потоки не смогут получить доступ к переменной balance, поэтому с точки зрения макросов конечный результат должен обеспечивать корректность. Но прервется ли операция посередине, мы не знаем. Дополнительные сведения см. в разделе Операции CAS.
PS: Вы можете быть немного смущены приведенным выше переменным балансом: почему переменный баланс должен добавляться с ключевым словом volatile? На самом деле цель добавления здесь ключевого слова volatile состоит в том, чтобы обеспечить видимость переменной balance и гарантировать, что каждый раз, когда вводится блок синхронизированного кода, самое последнее значение считывается из основной памяти.
Поэтому здесь
private volatile long balance;
Его также можно заменить синхронизированной модификацией.
private synchronized long balance;
Т.к. и это гарантирует наглядность, мы обсуждали в первой статьеСтранная видимость параллелизмабыл введен в.
2.4, Блокировка замка
public long deposit(long amount) {
readWriteLock.writeLock().lock();
try {
balance = balance + amount;
return balance;
} finally {
readWriteLock.writeLock().unlock();
}
}
Принцип блокировки блокировки для обеспечения атомарности аналогичен принципу синхронизации, поэтому я не буду здесь вдаваться в подробности.
Некоторым читателям может быть любопытно, у блокировки Lock есть операция снятия блокировки, а у синхронизированной вроде нет. На самом деле, компилятор Java автоматически добавит lock() и unlock() до и после синхронизированного измененного метода или блока кода. Преимущество этого в том, что lock() и unlock() должны появляться парами. для разблокировки unlock() является фатальной ошибкой (это означает, что другие потоки могут ждать бесконечно долго).
2.5 Типы атомарных операций
Если вы хотите определить свойства с атомарными классами, чтобы обеспечить правильность результатов, вам необходимо изменить классы сущностей следующим образом:
@Data
@AllArgsConstructor
public static class BankAccount {
private AtomicLong balance;
public long deposit(long amount) {
return balance.addAndGet(amount);
}
}
JDK предоставляет множество классов атомарных операций для обеспечения атомарности операций. Например, самые распространенные базовые типы:
AtomicBoolean
AtomicLong
AtomicDouble
AtomicInteger
Нижний уровень этих классов атомарных операций использует механизм CAS, который гарантирует, что вся операция присваивания является атомарной и не может быть прервана, тем самым обеспечивая правильность конечного результата.
По сравнению с синхронизированным, атомарный тип операции эквивалентен гарантии атомарности на микроскопическом уровне, в то время как синхронизированный гарантирует атомарность на макроскопическом уровне.
В приведенном выше решении 2.5 каждая небольшая операция является атомарной, например, операции модификации атомарных классов, таких как AtomicLong, их собственные грубые операции являются атомарными.
Итак, означает ли атомарность каждой маленькой операции, что вся композиция атомарна?
Очевидно нет.
Он по-прежнему создает проблемы с безопасностью потоков, как и весь процесс метода.读取A-读取B-修改A-修改B-写入A-写入B
; Тогда, если атомарность операции будет потеряна после завершения модификации A, а поток B начнет выполнять операцию чтения B в это время, возникнет проблема атомарности.
Короче говоря, не думайте, что при использовании потокобезопасных классов весь ваш код будет потокобезопасным! Все начинается с проверки общей атомарности вашего кода. Как в следующем примере:
@NotThreadSafe
public class UnsafeFactorizer implements Servlet {
private final AtomicReference<BigInteger> lastNum = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
@Override
public void service(ServletRequest request, ServletResponse response) {
BigInteger tmp = extractFromRequest(request);
if (tmp.equals(lastNum.get())) {
System.out.println(lastFactors.get());
} else {
BigInteger[] factors = factor(tmp);
lastNum.set(tmp);
lastFactors.set(factors);
System.out.println(factors);
}
}
}
Хотя все это использует атомарные классы для работы, операции не являются атомарными. То есть: например, когда поток A выполняет оператор else,lastNumber.set(tmp)
После этого, возможно, другие потоки выполнят оператор if.lastFactorys.get()
метод, а затем поток A продолжает выполнятьсяlastFactors.set(factors)
обновление методаfactors
!
Из этого логического процесса уже возникла проблема безопасности потока.
это нарушает метод读取A-读取B-修改A-修改B-写入A-写入B
В этом общем процессе после завершения записи A другие потоки выполняют чтение B, в результате чего считанное значение B не является записанным значением B. Так возникает атомарность.
Что ж, приведенное выше содержание является моим пониманием и кратким изложением атомарности в параллельном методе.Благодаря этим двум статьям мы примерно поняли общие проблемы видимости и атомарности в параллелизме и их общие решения.
Наконец
Опубликуйте часто встречающуюся проблему атомарного экземпляра.
просить: я часто слышу, как люди говорят, что добавление и вычитание длинных переменных на 32-битной машине таит в себе опасность параллелизма.
отвечать: утверждение о том, что сложение и вычитание длинных переменных на 32-разрядной машине сопряжено с потенциальными рисками параллелизма, верно.
Причина в том,: проблема атомарности, вызванная переключением потоков.
Неизменяемые переменные long и double имеют размер 8 байт и 64 бита.Когда 32-битная машина читает или записывает эту переменную, она должна быть разделена на две 32-битные операции.Возможно, поток прочитал старшие 32 бита определенного значения , Младшие 32 бита были изменены другим потоком. Поэтому официальная рекомендация — объявлять переменные longdouble как volatile или синхронизировать с блокировками, чтобы избежать проблем параллелизма.
Справочная статья:
- 1. Практика параллельного программирования на Java в свободное время
- 2. https://juejin.cn/post/6844903925749907463
- 3. https://www.cnblogs.com/54chensongxia/p/12073428.html
- 4. HTTPS://wuwuwu.big column.com/2019/10/04/5, метод 972 1 oh 314 ah/
Добро пожаловать в публичный аккаунт:Способ изучения Java
Сайт личного блога:www.mmzsblog.cn