предисловие
Если вы хотите поговорить о темах, связанных с атомарными классами, вы можете начать с основных понятий.
1. Какие проблемы решают атомарные классы?
Ответ: чтобы обеспечить согласованность данных одной переменной без блокировок в параллельных сценариях.
2. Когда возникает проблема параллелизма?
Ответ. Существует проблема многопоточного параллелизма, когда несколько потоков читают и записывают одни и те же общие данные одновременно.
Есть много способов решить проблему безопасности параллелизма,Самый известный из них — JDK concurrent package concurrent., существует для параллелизма
Прочтите статью, чтобы получить следующие знания:
1. Фон вывода программирования без блокировки 2. Как CAS реализует программирование без блокировок 3. «Болевые точки» ABA при использовании CAS 4. Как решить проблему «АВА»
Номер статьи начиная с паблика[Кружок интереса к исходному коду], обратите внимание на общедоступную учетную запись, чтобы впервые получить базовые знания, она была выпущена【44】статьиОригинальный технический пост в блоге
неатомарные вычисления
Каждый должен знать, что аналогично операции i++ в коде, хотя это и одна строка, выполнение разбито на три шага.
- Получить переменную i из основной памяти
- значение переменной i + 1
- После сложения значение переменной i записывается обратно в оперативную память.
Напишите небольшую программу, чтобы лучше понять, как бы мы ни запускали следующую программу, вероятность более 99% не достигнет 100000000
static int NUM = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
NUM++;
}
}).start();
}
Thread.sleep(2000);
System.out.println(NUM);
/**
* 99149419
*/
}
Вы можете использовать синхронизированный, который поставляется с JDK, путемМетод блокировки мьютексаВыполнить NUM ++ блок кода синхронно
static int NUM = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
synchronized (Object.class) {
NUM++;
}
}
}).start();
}
Thread.sleep(2000);
System.out.println(NUM);
/**
* 100000000
*/
}
Я говорил о замках, а AtomicInteger говорит о замках Мы не можем продавать собачье мясо на овечьих шкурах, верно?
Если вы видели исходный код библиотеки классов в пакете JUC исходного кода JDK, оБиблиотеки классов, начиная с Atomicне должен быть незнакомым
Atomic Английский [əˈtɒmɪk] Американский [əˈtɑːmɪk] Перевод : atomic
Если вы не используете блокировки для решения вышеуказанной проблемы неатомарного автоинкремента, вы можете написать это так:
static AtomicInteger NUM = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
// 🚩 重点哦, 自增并获取新值
NUM.incrementAndGet();
}
}).start();
}
Thread.sleep(2000);
System.out.println(NUM);
/**
* 100000000
*/
}
Введение в AtomicInteger
Когда вы видите относительно новое знание, такое как техническая точка или структура, вы обычно думаете о двух вопросах.
Что это
AtomicInteger предоставляется в параллельном пакете JDK.Управление атомарным классом целочисленного типа, вызывая базовыйМетоды Unsafe, связанные с CAS, реализуют атомарные операции.
На основании неатомной оптимистичности блокировки идей реализована операция,Гарантировать потокобезопасность одной переменной в случае многопоточности
Концепция CAS будет подробно объяснена ниже.
Каковы преимущества
AtmoicInteger использует инструкции аппаратного уровня CAS для обновления значения счетчика, инструкции, напрямую поддерживаемые машиной,Это позволяет избежать блокировки
Например, в случае серьезного параллелизма, такого как синхронизированная блокировка взаимного исключения, блокировка будет заблокирована.Обновление до тяжелого замка
При пробуждении и блокировке потоков будетПереход из пользовательского режима в режим ядра, а переходное состояние занимает много времени
Я написал программу для тестирования.Хотя синхронизация была обновлена, производительность значительно улучшилась, но вВ общих параллельных сценариях алгоритм CAS без блокировки имеет более высокую производительность.
Конечно, невозможно сделать идеальный подарок, о алгоритмах CAS lock-free, описанных ниже, где слабые места
Структурный анализ
AtomicInteger имеет два конструктора: конструктор без параметров и конструктор с параметрами.
- Значение, созданное без аргументов, является значением по умолчанию для int 0.
- Параметризованная конструкция присвоит значение
public AtomicInteger() { }
public AtomicInteger(int initialValue) {
value = initialValue;
}
AtomicInteger имеет три важные переменные, а именно:
Unsafe:Можно понять, что это "ОШИБКА" для Java.Самая большая роль в AtomicInteger заключается в прямом манипулировании памятью для замены значения.
value:Используйте тип int для хранения значения, вычисленного AtomicInteger, украшенного volatile,Обеспечение видимости памяти и предотвращение переупорядочения инструкций
valueOffset:смещение памяти значения
// 获取Unsafe实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
// 静态代码块,在类加载时运行
static {
try {
// 获取 value 的内存偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
private volatile int value;
Вот некоторые часто используемые API, основные идеи реализации те же, и одна из них будет подчеркнута.
// 获取当前 value 值
public final int get();
// 取当前的值, 并设置新的值
public final int getAndSet(int newValue);
// 获取当前的值, 并加上预期的值
public final int getAndAdd(int delta);
// 获取当前值, 并进行自增1
public final int getAndIncrement();
// 获取当前值, 并进行自减1
public final int getAndDecrement();
Получить текущее значение и увеличить его #getAndIncrement()
Посмотрите, как работает конкретный в исходном коде
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
* unsafe.getAndAddInt
*
* @param var1 AtomicInteger 对象
* @param var2 value 内存偏移量
* @param var4 增加的值, 比如在原有值上 + 1
* @return
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 内存中 value 最新值
var5 = this.getIntVolatile(var1, var2);
} while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
Именно здесь CAS воплощает алгоритм без блокировки.Давайте сначала поговорим об этапах выполнения этого кода.
1. Получите новейшее значение соответствующего значения в соответствии с объектом AtomicIntheger и смещением значения Value Memory
2. Измените значение в памяти (var5) на ожидаемое значение (var5+var4) с помощью compareAndSwapInt(...), многопоточной конкуренции нет, успешно измените и верните True, чтобы завершить цикл, и Flase продолжит выполнение петля
Суть в том, чтоcompareAndSwapInt(...)
/**
* 比较 var1 的 var2 内存偏移量处的值是否和 var4 相等, 相等则更新为 var5
*
* @param var1 AtomicInteger 对象
* @param var2 value 内存偏移量
* @param var4 value 原本的值
* @param var5 期望将 value 设置的值
* @return
*/
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
Поскольку это модификация нативного ключевого слова, мы не можем просмотреть его исходный код и объяснить идеи метода.
1. Получите значение от var2 (смещение памяти) до var1 (AtomicInteger)
2. Сравните значение (значение в памяти) с var4 (значение, полученное в потоке)
3. Если равно, установите var5 (ожидаемое значение) на новое значение в памяти и верните True.
4, не равным возврату FALSE продолжает пытаться выполнить цикл
Графический анализ CAS
Вот набор картинок для дальнейшего понимания CAS
Unsafe#getAndAddInt()Возьмите этот метод в качестве примера
1. Это изображение эквивалентно позиции valueOffset, соответствующей объекту AtomicInteger.
2. Первый поток выполняет var5 = this.getIntVolatile(var1, var2)
3. Поток 2 выполняет var5 = this.getIntVolatile(var1, var2)
В это время VAR5 равен 0 в одном потоке, а во втором работает память.
4, нить будет изменена в качестве значения памяти, сравнивая значение памяти VAR4 равное значение памяти успеха, установлено на 1
5. Поток 2 также хочет изменить соответствующее значение value в памяти.Если обнаружится, что оно не равно, вернуть False и продолжить попытки изменить его.
недостаточность
Хотя CAS может обеспечить программирование без блокировок и повысить производительность в целом, он не лишен ограничений и недостатков.
1. Накладные расходы процессора велики
В случае высокого параллелизма, если спин-CAS не работает в течение длительного времени, это приведет к очень большим накладным расходам на выполнение ЦП.
Если вам нужно добиться подсчета в условиях высокого параллелизма, вы можете использовать LongAdder, дизайн высокого параллелизма действительно силен!
2. Знаменитая проблема «АВА».
CAS должен проверить, изменилось ли значение при работе со значением, и обновить его, если изменений нет.
Но если значение изначально было A, стало B, а затем стало A, то при проверке CAS обнаружит, что его значение не изменилось, а изменилось на самом деле.
Если вам интересно, вы можете взглянуть на пакет JUCA atomic.AtomicStampedReference
Предыстория проблемы ABA
Проблема с AtomicInteger, которая также существует в большинстве классов, связанных с Atomic, — это проблема ABA.
Короче говоря, это нить, чтобы получить AtomicInteger значение 0, пока вы не будете готовы внести изменения
Нить две пары AtomicInteger делают две операции, как только значение изменено на 1, затем измените значение в оригинал 0
В это время, как только поток выполняет операцию CAS, обнаруживается, что значение в памяти по-прежнему равно 0, OK, и обновление прошло успешно.
1. Почему второй поток может работать после того, как первый поток получит последнее значение?
мы начинаем сAtomicInteger#getAndIncrementинструкция
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 标记1
var5 = this.getIntVolatile(var1, var2);
} while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
Поскольку выполнение нашего процессораПревентивное выделение кванта времени не фиксировано
Возможно, после того, как поток прочитает метку 1, квант времени будет выделен для выполнения потока 2, а поток 1 будет ждать выделения кванта времени.
После того, как второй поток завершит двухэтапную операцию, квант времени выделяется первому потоку, после чего выполнение продолжается.
2. Какие проблемы вызывает ABA?
Каждый должен знать, как происходит поведение ABA.Если перечислить небольшой пример в Интернете, общий смысл таков:
1. Баланс счета банковской карты Сяоминга составляет 10 000 юаней, и он отправляется в банкомат, чтобы снять 5000 юаней. Обычно следует сделать один запрос, но были сделаны два запроса из-за сбоя сети или механизма.
Братья и сестры, пожалуйста, не завышайте планку, ничего не говорите об идемпотентности бэкенд-интерфейса, антидубликатной подаче банкомата и т.д.Пример иллюстрирует проблему, спасибо 😄
2. Поток 1 «Инициирован банкоматом»: получите текущее значение баланса банковской карты в размере 10 000 юаней и ожидаемое значение в размере 5 000 юаней.
3. Поток 2 «Инициирован банкоматом»: получите текущее значение баланса банковской карты в размере 10 000 юаней и ожидаемое значение в размере 5 000 юаней.
4. Поток 1 проходит успешно, баланс на банковской карте остается 5000, а поток 2 не выделяет квант времени, и блокируется после получения баланса
5, в это время нить три «Alipay передается на банковскую карту»: получить баланс нынешнего банка в 5000 юаней, ожидал 10 000 юаней, успешной ревизии
6. Поток 2 получает квант времени, обнаруживает, что баланс на карте равен 10 000, и успешно снижает баланс на карте до 5 000
7. Первоначальный баланс на карте должен быть 10 000-5 000+5 000=10 000, но в итоге из-за проблемы с ABA он будет 10 000-5 000+5 000-5 000=5 000
Конечно, в формальном бизнесе таких проблем может и не быть, но если вы обычно используете атомарные классы в своем бизнесе, потенциальные скрытые опасности все равно будут.
Давайте сначала посмотрим на небольшой программный код, чтобы увидеть, как воспроизводится проблема ABA.
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger(100);
new Thread(() -> {
atomicInteger.compareAndSet(100, 101);
atomicInteger.compareAndSet(101, 100);
}).start();
Thread.sleep(1000);
new Thread(() -> {
boolean result = atomicInteger.compareAndSet(100, 101);
System.out.println(String.format(" >>> 修改 atomicInteger :: %s ", result));
}).start();
/**
* >>> 修改 atomicInteger :: true
*/
}
1. Создайте AtomicInteger с начальным значением 100, поток будет 100->101, а затем от 101->100
2. Спящий режим на 1000 мс, чтобы предотвратить преждевременное выполнение потока 2 распределения временных интервалов.
3. Тема 2 из 100->101, модификация прошла успешно
AtomicStampedReference
Давайте сначала поговорим об идее решения ABA, то естьAtomicStampedReferenceпринцип
поддерживать внутреннююПара объектов, сохраняет значение значения и номер версии, каждое обновление будет обновлять номер версии в дополнение к значению значения
private static class Pair<T> {
// 存储值, 相当于上文的值100
final T reference;
// 类似于版本号的概念
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
// 创建一个新的Pair对象, 每次值变化时都会创建一个新的对象
static <T> AtomicStampedReference.Pair<T> of(T reference, int stamp) {
return new AtomicStampedReference.Pair<T>(reference, stamp);
}
}
Давайте сначала пройдемся по небольшой программе, чтобы понятьAtomicStampedReferenceРабочий механизм
@SneakyThrows
public static void main(String[] args) {
AtomicStampedReference stampedReference = new AtomicStampedReference(100, 0);
new Thread(() -> {
Thread.sleep(50);
stampedReference.compareAndSet(100,
101,
stampedReference.getStamp(),
stampedReference.getStamp() + 1);
stampedReference.compareAndSet(101,
100,
stampedReference.getStamp(),
stampedReference.getStamp() + 1);
}).start();
new Thread(() -> {
int stamp = stampedReference.getStamp();
Thread.sleep(500);
boolean result = stampedReference.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println(String.format(" >>> 修改 stampedReference :: %s ", result));
}).start();
/**
* >>> 修改 atomicInteger :: false
*/
}
1. Создайте AtomicStampedReference и установите начальное значение 100 и номер версии 0.
2. Поток спит 50 мс, а затем работает со значением 100->101, 101->100, версия +1.
Зачем спать 50 мс? Чтобы имитировать многопоточное параллельное вытеснение, пусть второй поток сначала получит номер версии.
3. Поток 2 засыпает на 500 мс, ждет завершения выполнения потока 1 и начинает устанавливать 100->101, номер версии +1.
Неудивительно, что второй поток определенно не сможет модифицировать, хотя значения соответствуют, ожидаемый номер версии не соответствует одному в паре
compareAndSet
Посмотрите на его compareAndSet(...) как это сделать
/**
* 比较并设置
*
* @param expectedReference 预期值
* @param newReference 期望值
* @param expectedStamp 预期版本号
* @param newStamp 期望版本号
* @return 是否成功
*/
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
// 获取当前 pair 引用
AtomicStampedReference.Pair<V> current = pair;
// 预期值对比 pair value
return expectedReference == current.reference &&
// 预期版本号对比 pair stamp
expectedStamp == current.stamp &&
// 期望值对比 pair value
((newReference == current.reference &&
// 期望版本号对比 pair stamp
newStamp == current.stamp) ||
// casPair
casPair(current, Pair.of(newReference, newStamp)));
}
Если compareAndSet(...) True, вышеуказанные условия должны удовлетворять выражению три условия
1. Ожидаемое значение равно парному значению
2. Ожидаемый номер версии равен штампу пары
3. Ожидаемое значение равно значению пары, а ожидаемый номер версии равен отметке пары.
Это сценарий, когда значение и номер версии не изменились
4. Когда первое условие и второе условие выполняются, но третье условие не выполняется, значение подтверждения и номер версии изменились, и создается пара для сравнения и замены CAS.
Вышеупомянутые условия должны соответствовать 1, 2, 3 или соответствовать 1, 2, 4, могут возвращать True
Переключите текущую атомарную ссылку Pair на новую Pair с помощьюAtomicReference имеет ту же идею, атомарно преобразует ссылку на объект
private boolean casPair(AtomicStampedReference.Pair<V> cmp, AtomicStampedReference.Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
Яма с использованием целочисленного значения
Друзья должны знать, что если вы напишете следующий код, объекты не будут создаваться в куче
Integer i1 = 100;
Integer i2 = 101;
Потому что JVM в постоянном пуле хранения двух объектов, и больше, чем значение -128-127, создаст объекты в куче
Ссылка на нашу паре объект представляет собой универсальный тип, а прошедшее значение типа int будет преобразовано в целое число
Если используется тип Integer и значение не находится в диапазоне от -128 до 127, произойдет ошибка сравнения данных.Вы можете увидеть compareAndSet(...)
AtomicMarkableReference
Вы можете видеть, что две операции с указанным выше номером версии должны быть несогласованными, и они должны быть увеличены или уменьшены или другие операции, которые относительно громоздки.
Мастер Дуг Ли также предоставляет нам удобную библиотеку классов.AtomicMarkableReference
Интерфейс API и идеи реализации в основном такие же, как и выше, за исключением того, что тип int номера версии заменен на тип boolean, а в других нет никакой разницы.
но,AtomicMarkableReference не решает полностью проблему ABA, но с небольшой вероятностью может ее предотвратить
Вывод
В связи с ограниченным уровнем автора, приветствуются отзывы и исправления неточностей в статье, спасибо 🙏
Лайк моих друзей - самая большая поддержка для меня.Если я что-то почерпнул из прочтения статьи, то надеюсь, что смогуСтавьте лайки, комментируйте, подписывайтесь на Санлиан!
Рекомендуемое чтение:
- [настоятельно рекомендуется] Будьте осторожны при использовании ParallelStream, новой функции JDK 8.
- [Настоятельно рекомендуется] Быстро понять, как Redisson реализует принцип распределенных блокировок.
- [Настоятельно рекомендуется] Поговорим о ReentrantLock и AQS.
- [Вопросы интервью с Дачангом] Как быстро выполнять задачи, не превышая максимальное количество потоков в пуле потоков JDK
- [Вопросы интервью с Дачангом] Как пул потоков JDK гарантирует, что основные потоки не будут уничтожены
Автор Ма Хуа — программист-Дева, который координирует разработку серверной части Java в Imperial Capital, вдохновляющий программист-Дева, который фокусируется на высоком уровне параллелизма, базовом исходном коде фреймворка и распределенном обмене знаниями.