i++ не является потокобезопасной операцией, потому что это не атомарная операция.
Итак, если я хочу добиться такого же эффекта, как i++, какие коллекции или служебные классы мне следует использовать?
До JDK1.5, чтобы гарантировать, что определенный基本
тип данных или引用
Атомарность операций с типами данных должна основываться на иностранных ключевых словах.synchronized
, но эта ситуация изменилась после JDK1.5. Конечно, вы все еще можете использовать synchronized для обеспечения атомарности. Поточно-ориентированный способ, о котором мы говорим здесь, — это атомарные классы инструментов, такие какAtomicInteger, AtomicBooleanЖдать. Эти атомарные классы являются потокобезопасными служебными классами, и они такжеLock-Free
из. Давайте посмотрим на эти классы инструментов и на то, что такое концепция Lock-Free.
Узнать об AtomicInteger
AtomicInteger
Это недавно добавленный инструментальный класс в JDK 1.5. Давайте сначала рассмотрим его отношения наследования.

Как и класс-оболочка Integer для int, он наследуется отNumber
Категория.

Класс Number — это класс-оболочка для базовых типов данных.Как правило, объекты, связанные с типами данных, наследуются от класса Number.
Его система наследования очень проста, давайте рассмотрим ее основные свойства и методы.
Основные свойства AtomicInteger
Есть три основных свойства AtomicInteger

Unsafe
даsun.misc
Класс ниже пакета, AtomicInteger, в основном полагается на некоторые собственные методы, предоставляемые sun.misc.Unsafe, для обеспечения атомарности операций.
НебезопасноobjectFieldOffset
Метод может получить смещение адреса свойства члена в памяти относительно адреса памяти объекта. Проще говоря, это найти адрес этой переменной в памяти, что удобно для последующих операций непосредственно через адрес памяти.value
Мы поговорим об этом позже
value
является значением AtomicIneger.
Конструктор AtomicInteger
Продолжая смотреть вниз, есть только два конструктора AtomicInteger.Один является конструктором без параметров.Значение конструктора без параметров по умолчанию равно 0. Конструктор с параметрами может указывать начальное значение.

Методы в AtomicInteger
Давайте поговорим о методах в AtomicInteger.
Получить и установить
Давайте сначала рассмотрим простейшие методы get и set:
get()
: получить значение текущего AtomicInteger
set()
: установить значение текущего AtomicInteger
get() может атомарно считывать данные в AtomicInteger, а set() может атомарно устанавливать текущее значение, потому что и get(), и set() в конечном итоге воздействуют на переменную значения, а значение определяетсяvolatile
Модифицировано, поэтому получение и установка эквивалентны чтению и настройке памяти. Как показано ниже

Выше мы упомянули неатомарные операции i++ и i++ и сказали, что вместо них можно использовать методы из AtomicInteger.
Инкрементная операция
AtomicInteger вIncremental
Связанные методы могут удовлетворить наши потребности
-
getAndIncrement()
: атомарно увеличивает текущее значение и возвращает результат. эквивалентноi++
операция.
Чтобы проверить, является ли он потокобезопасным, мы используем следующий пример для проверки
public class TAtomicTest implements Runnable{
AtomicInteger atomicInteger = new AtomicInteger();
@Override
public void run() {
for(int i = 0;i < 10000;i++){
System.out.println(atomicInteger.getAndIncrement());
}
}
public static void main(String[] args) {
TAtomicTest tAtomicTest = new TAtomicTest();
Thread t1 = new Thread(tAtomicTest);
Thread t2 = new Thread(tAtomicTest);
t1.start();
t2.start();
}
}
Вы обнаружите, что это потокобезопасная операция, выводя результат, вы можете изменить значение i, но окончательный результат по-прежнему равен i - 1, потому что сначала берется значение, а затем + 1, его схематическая диаграмма выглядит так: следует.

-
incrementAndGet
Напротив, сначала выполняется операция + 1, а затем возвращается результат с автоматическим приращением, что обеспечивает атомарную операцию над значением. Как показано ниже

Декрементная операция
Напротив, операция уменьшения, такая как x-- или x = x-1, является атомарной. Мы все еще можем использовать методы в AtomicInteger для замены
-
getAndDecrement
: возвращает значение int текущего типа, затем уменьшает значение value. Ниже приведен тестовый код
class TAtomicTestDecrement implements Runnable{
AtomicInteger atomicInteger = new AtomicInteger(20000);
@Override
public void run() {
for(int i = 0;i < 10000 ;i++){
System.out.println(atomicInteger.getAndDecrement());
}
}
public static void main(String[] args) {
TAtomicTestDecrement tAtomicTest = new TAtomicTestDecrement();
Thread t1 = new Thread(tAtomicTest);
Thread t2 = new Thread(tAtomicTest);
t1.start();
t2.start();
}
}
Ниже приведена схема getAndDecrement.

-
decrementAndGet
: Точно так же метод decrementAndGet заключается в том, чтобы сначала выполнить операцию уменьшения, а затем получить значение значения.Схема выглядит следующим образом.

Метод LazySet
Знаете ли вы, что у volatile есть барьеры памяти?
Что такое барьер памяти?
Барьер памяти, также известный как
内存栅栏
, барьер памяти, барьерная инструкция и т. д. — тип барьерной инструкции синхронизации, которая представляет собой точку синхронизации в работе ЦП или компилятора при произвольном доступе к памяти, так что все операции чтения и записи до этой точки могут быть выполнены прежде чем они могут быть выполнены Действия после этой точки. Это также способ сделать состояние памяти процессора ЦП видимым для других процессоров.
ЦП использует множество оптимизаций, используя кэши, переупорядочивание инструкций и т. д. Конечной целью является повышение производительности, то есть когда программа выполняется, пока конечный результат один и тот же, не имеет значения, являются ли инструкции перезаказывал или нет. Поэтому тайминг выполнения инструкций выполняется не последовательно, а выполняется не по порядку, что принесет много проблем, что также способствует возникновению барьеров памяти.
Семантически все операции записи до барьера памяти записываются в память; операции чтения после барьера памяти могут получить результаты операций записи до барьера синхронизации. Поэтому для чувствительных блоков барьер памяти может быть вставлен после операции записи и перед операцией чтения.
Накладные расходы на барьеры памяти очень легкие, но какими бы маленькими они ни были, накладные расходы есть.LazySet как раз то, что он делает.Он будет читать и записывать переменные в виде обычных переменных.
Можно также сказать, что:Лень ставить барьеры

Метод GetAndSet
Атомарно установить заданное значение и вернуть старое значение.
Его исходный код заключается в вызове метода getAndSetInt в небезопасном режиме, как показано ниже.

сначала зациклить, а затем вызватьgetIntVolatile
метод, я не нашел этот метод в cpp, друзья, которые нашли его, не забудьте сообщить мне вовремя, чтобы изучить его.
Цикл до тех пор, пока compareAndSwapInt не вернет false, что означает, что использование CAS не обновило новое значение, поэтому var5 возвращает последнее значение памяти.
CAS-метод
CAS, о котором мы говорили, на самом делеCompareAndSet
Метод, как следует из названия,Сравните и обновитеСмысл , конечно, это буквальное понимание, а буквальное понимание немного предвзятое.На самом деле то, что они означают, это сначала сравнить, а затем обновить, если устраивает.

Исходный код уровня CAS Java приведен выше, а официальное объяснение JDK для него —Если текущее значение равно значению expect , то атомарно установите текущее значение для обновления заданного значения., этот метод вернет логический тип, если это правда, это означает, что сравнение и обновление прошли успешно, в противном случае это означает сбой.
CAS также является механизмом параллелизма без блокировки, также известным какLock Free
, так вы думаете, Lock Free это здорово? нисколько.
Далее строим замок и разблокируемCASLock
class CASLock {
AtomicInteger atomicInteger = new AtomicInteger();
Thread currentThread = null;
public void tryLock() throws Exception{
boolean isLock = atomicInteger.compareAndSet(0, 1);
if(!isLock){
throw new Exception("加锁失败");
}
currentThread = Thread.currentThread();
System.out.println(currentThread + " tryLock");
}
public void unlock() {
int lockValue = atomicInteger.get();
if(lockValue == 0){
return;
}
if(currentThread == Thread.currentThread()){
atomicInteger.compareAndSet(1,0);
System.out.println(currentThread + " unlock");
}
}
public static void main(String[] args) {
CASLock casLock = new CASLock();
for(int i = 0;i < 5;i++){
new Thread(() -> {
try {
casLock.tryLock();
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}finally {
casLock.unlock();
}
}).start();
}
}
}
В приведенном выше коде мы создаем CASLock, вtryLock
В методе мы сначала используем метод CAS для обновления, если обновление не удается, генерируется исключение, и текущий поток устанавливается в качестве потока блокировки. существуетunLock
В методе мы сначала оцениваем, равно ли текущее значение 0, если оно равно 0, это результат, который мы хотим увидеть, и возвращаемся напрямую. В противном случае это 1, что означает, что текущий поток все еще заблокирован.Давайте оценим, является ли текущий поток заблокированным потоком, и если да, выполните операцию разблокировки.
Затем сравнение и набор, о котором мы упоминали выше, можно разобрать на следующие операции:
// 伪代码
// 当前值
int v = 0;
int a = 0;
int b = 1;
if(compare(0,0) == true){
set(0,1);
}
else{
// 继续向下执行
}
Вы также можете взять пример покупки билетов в жизненных сценариях.У вас должен быть билет, чтобы войти в живописное место.Если у вас есть поддельный билет или билет, который не соответствует живописному месту, вас обязательно могут идентифицировать.Вы обязательно будете не иметь возможности войти в живописное место.
Без лишних слов, вот принципиальная схема compareAndSet

-
weakCompareAndSet
: Блин, я несколько раз перечитал очень серьезно, и обнаружил, что этот метод JDK1.8 точно такой же, как метод compareAndSet. . .

Но так ли это на самом деле? Нет, исходный код JDK очень обширен и глубок, поэтому он не будет разрабатывать повторяющийся метод.Вы думаете, что команда JDK не будет фиксировать такую низкоуровневую команду, но в чем причина?
Книга "Подробное объяснение Java High Concurrency" дает нам ответ

AddAndGet
AddAndGet и getAndIncrement, getAndAdd, incrementAndGet и другие методы используют операцию do ... while + CAS, которая фактически эквивалентна спин-блокировке. возвращается. Схема выглядит следующим образом

Погрузитесь в AtomicInteger
Мы обсуждали конкретное использование AtomicInteger выше и знаем, что AtomicInteger полагается на volatile и CAS для обеспечения атомарности, поэтому давайте проанализируем, почему CAS может гарантировать атомарность и каков его базовый уровень? Какое отношение AtomicInteger имеет к оптимистичной блокировке?
Основной принцип реализации AtomicInteger
Давайте посмотрим на этого милогоcompareAndSetL(CAS)
метод, почему атомарность гарантируется только для этих двух строк кода?

Мы видим, что этот метод CAS эквивалентен вызову unsafe вcompareAndSwapInt
метод, давайте перейдем к небезопасному, чтобы увидеть конкретную реализацию.

сравнитеAndSwapIntsun.misc
метод в , этот метод являетсяnative
метод, его нижний слой реализован на C/C++, поэтому нам нужно посмотреть исходный код C/C++.
Знаете ли вы, в чем прелесть C/C++? Использование Java — это игра с приложением и архитектурой, а C/C++ — игра с сервером и нижним уровнем.
Исходный код compareAndSwapInt находится вjdk8u-dev/hotspot/src/share/vm/prims/unsafe.app
По пути его реализация исходного кода

этоUnsafe_CompareAndSwapInt
метод, мы находим этот метод

Я не могу понять исходный код C/C++, но это не мешает нам найти ключевой кодAtomic::cmpxchg
, cmpxchg — это ассемблерная инструкция для архитектуры процессора x86, ее основная функция — сравнение и обмен операндами. Давайте продолжим вниз, чтобы найти определение этой команды.
Мы обнаружим, что для разных ОС базовая реализация отличается.

Мы нашли реализацию Windows следующим образом

Продолжаем смотреть вниз, он собственно и определяет код на 216 строке, находим его

На этом этапе требуется знание инструкций по сборке и регистров.
вышеos::is-MP()
— это интерфейс многопроцессорной операционной системы, ниже — __asm, ключевое слово C/C++, используемое для вызова встроенного ассемблера.
Код на __asm представляет собой ассемблер, который грубо раскладывает значения dest, exchange_value и compare_value в регистры.LOCK_IF_MP
Общий смысл кода таков.

Если это мультипроцессор, будет выполнена блокировка, а затем будет выполнена операция сравнения. Среди них cmp означает сравнение, а mp означает, чтоMultiProcess
,je
Представляет равный переход, а L0 представляет бит флага.
Возвращаемся к приведенной выше инструкции по сборке, мы видим, что нижний слой CAScmpxchg
инструкция.
оптимистическая блокировка
У вас есть этот вопрос, почему AtomicInteger может получить текущее значение, тогда почему оно все еще появляетсяexpectValue
иvalue
А несоответствия?
Поскольку AtomicInteger — это всего лишь атомарный служебный класс, он не является исключительным, он не похож наsynchronized
илиlock
Так же, как взаимоисключающие и исключающие, помните, что в AtomicInteger есть два метода get и set? они просто используютvolatile
Modified и volatile не являются атомарными, поэтому могут быть несоответствия между текущим значением expectValue и value, поэтому могут возникать повторные модификации.
Есть два решения вышеописанной ситуации, одно из них — использоватьsynchronized
иlock
Подобный механизм блокировки, этот тип блокировки является эксклюзивным, то есть только один поток может изменять его одновременно.Этот метод может гарантировать атомарность, но относительные накладные расходы относительно велики.Такой тип блокировки также называется пессимистической блокировкой. . Другим решением является использование版本号
илиCAS 方法
.
номер версии
Механизм номера версии заключается в добавленииversion
Это реализуется полем, указывающим, сколько раз данные были изменены.Когда операция записи выполнена и запись прошла успешно, версия = версия + 1. Когда поток А захочет обновить данные, он также прочитает значение версии при чтении данных.При отправке обновления, если только что прочитанное значение версии равно значению версии в текущей базе данных, оно будет обновлено; в противном случае операция обновления будет повторяться до тех пор, пока обновление не будет успешным.
CAS-метод
Другой способ - CAS. Мы использовали много места, чтобы представить метод CAS. Тогда мы думаем, что теперь у вас есть определенное понимание механизма его работы, и мы не будем объяснять его механизм работы.
У всего есть свои преимущества и недостатки.В индустрии программного обеспечения нет идеального решения, есть только оптимальное решение.Поэтому оптимистическая блокировка также имеет свои недостатки и недостатки, то есть проблему ABA.
АВА-проблема
Проблема ABA заключается в том, что если значение переменной, прочитанной в первый раз, равно A, а когда она готова к записи в A, обнаруживается, что значение по-прежнему равно A, то в этом случае можно считать, что значение переменной равно A. значение A не изменилось. ? Это может быть случай A -> B -> A , но AtomicInteger так не думает, он просто верит в то, что видит, и видит то, что видит. Например
Если теперь есть односвязный список, как показано на следующем рисунке

A.next = B, B.next = null, в это время два потока T1 и T2 извлекают из односвязного списка соответственно A. По каким-то особым причинам T2 изменяет A на B, а затем снова на A. При этом время, T1 Выполните метод CAS и обнаружите, что односвязный список по-прежнему A, и метод CAS будет выполнен.Хотя результат правильный, эта операция вызовет некоторые потенциальные проблемы.

В настоящее время это все еще односвязный список.Два потока T1 и T2 соответственно извлекают A из односвязного списка, а затем T1 изменяет связанный список на ACD, как показано на следующем рисунке.

В это время T2 обнаруживает, что значение памяти по-прежнему равно A, и попытается заменить значение A значением B, поскольку ссылка B равна нулю, что приведет к тому, что C и D в это время будут в свободном состоянии.

JDK 1.5 и вышеAtomicStampedReference
класс предоставляет эту возможность, в которойcompareAndSet
Метод заключается в том, чтобы сначала проверить, равно ли текущее значение ожидаемому значению. Критерием для оценки является то, что текущая ссылка и штемпель равны ожидаемой ссылке и почтовому штемпелю соответственно. Если они все равны, они атомарно устанавливаются в заданное значение обновления.

Что ж, приведенный выше поток кода Java Увидев натив, мы понимаем, что снова собираемся шлепнуть cpp. открыть

Просто объяснитеUnsafeWrapper
Это просто обертка, просто другое имя. Затем, после некоторой обработки JNI, поскольку compareAndSwapOject сравнивает ссылки, он должен пройти объектно-ориентированное преобразование C++. Основным методом являетсяatomic_compare_exchange_oop

Как видите, снова появляются знакомые словаcmpxchg
, то есть compareAndSwapOject использует атомарную инструкцию cmpxchg, но она претерпела ряд преобразований.
постскриптум
Возникает вопрос, может ли CAS гарантировать видимость между переменными? Зачем?
еще есть вопрос,getIntVolatile
Где источник cpp метода? Как его найти?
Если большие ребята выше заинтересованы в этих двух вопросах, добро пожаловать в общение.
Обратите внимание на официальный аккаунт.Программист cxuan отвечает на cxuan для получения качественной информации.
Я сам написал шесть PDF-файлов, очень хардкорно, ссылки ниже
Я сам написал шесть PDF-файлов, очень хардкорно, ссылки ниже
Я сам написал шесть PDF-файлов, очень хардкорно, ссылки ниже