предисловие
Говоря оCAS(CompareAndSwap), сначала я должен кое-что сказатьпессимистический замока такжеоптимистическая блокировкапотому что CAS - это реализация оптимистической идеи блокировки.
пессимистический замок: всегда пессимистично, что другие потоки будут выполняться одновременно каждый раз, когда берутся данные, поэтому блокировка будет добавляться каждый раз, и блокировка будет снята после того, как данные будут израсходованы, так что другие потоки могут получить блокировку, а затем получить ресурсы для работы. Эксклюзивные блокировки, такие как synchronized и ReentrantLock в java, являются реализацией идеи пессимистической блокировки.
оптимистическая блокировка: я всегда с оптимизмом смотрю на то, что когда я получаю операцию с данными, нет других потоков, которые будут работать одновременно.Когда я хочу обновить данные после завершения моей операции, я буду судить, есть ли другие потоки, работающие во время операции с данными, и если это так, сделайте это. Повторяйте попытку, пока изменение операции не будет успешным. Оптимистическая блокировка часто реализуется с использованием CAS и механизма номера версии. Джаваjava.util.atomicВсе атомарные классы в пакете реализованы на основе CAS.
1. Что такое КАС
КАС относится кCompareAndSwap, как подсказывает название,Сравните, а затем поменяйте. Сравнить что? В обмен на что?
В CAS есть три переменные: адрес памяти V, ожидаемое значение A и обновленное значение B.
Тогда и только тогда, когда значение, соответствующее адресу памяти V, равно ожидаемому значению A, замените значение, соответствующее адресу памяти V, на B.
Во-вторых, атомный пакет
Зная о пессимистической блокировке и оптимистичной блокировке, давайте перейдем к пакету java.util.atomic и посмотрим на реализацию CAS в java.
Этоjava.util.atomicДля классов под пакетом мы ориентируемся на исходный код AtomicInteger (остальные реализованы таким же образом)
Тогда подумайте, какие недостатки у CAS? Как решить недостатки? Каковы плюсы и минусы?
2.1. Введите исходный код AtomicInteger
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 使用Unsafe.compareAndSwapInt进行原子更新操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
//value对应的存储地址偏移量
private static final long valueOffset;
static {
try {
//使用反射及unsafe.objectFieldOffset拿到value字段的内存地址偏移量,这个值是固定不变的
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//volatile修饰的共享变量
private volatile int value;
//..........
}
Приведенный выше код фактически инициализирует смещение адреса памяти valueOffset, соответствующее значению памяти, что удобно для последующих операций CAS. Поскольку после инициализации это значение не изменится, поэтому используйте статическое финальное оформление.
Мы видим, что значение изменено с помощью volatile, последней статьи 9 драконов.Подробное объяснение JMM, в котором также говорится о семантике volatile, те, кто не понимает, могут сначала взглянуть.
Все мы знаем, что если выполняется операция value++, параллелизм небезопасен. В предыдущей статье мы также доказали на примерах, что volatile может гарантировать только видимость, а не атомарность. потому чтоvalue++ само по себе не является атомарной операцией, value++ делится на три шага: сначала получить значение value, выполнить +1, а затем присвоить его обратно значению.
2.2, сравнитьAndSwapXxx
Давайте сначала рассмотрим операции CAS, предоставляемые AtomicInteger.
/**
* 原子地将value设置为update,如果valueOffset对应的值与expect相等时
*
* @param expect 期待值
* @param update 更新值
* @return 如果更新成功,返回true;在valueOffset对应的值与expect不相等时返回false
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
Мы уже знаем принцип CAS, поэтому давайте взглянем на следующий тест.Вы знаете, каков результат вывода?Дайте свой ответ в разделе комментариев.
public class AtomicIntegerTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.compareAndSet(0, 1);
atomicInteger.compareAndSet(2, 1);
atomicInteger.compareAndSet(1, 3);
atomicInteger.compareAndSet(2, 4);
System.out.println(atomicInteger.get());
}
}
Unsafe предоставляет три метода атомарных обновлений.
Что касается класса Unsafe, поскольку java не поддерживает прямое манипулирование базовыми аппаратными ресурсами, например выделение памяти и т. д. Если вы используете unsafe для открытия памяти, она не управляется сборщиком мусора JVM и должна управляться вами самостоятельно, что легко может вызвать утечку памяти и т. д.
2.3, метод атомарного приращения AtomicInteger
Как мы сказали выше, value++ не является атомарной операцией и не может использоваться в параллелизме. Давайте посмотрим на операции atomic++, предоставляемые AtomicInteger.
/**
* 原子地对value进行+1操作
*
* @return 返回更新后的值
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
/**
* unsafe提供的方法
* var1 更改的目标对象
* var2 目标对象的共享字段对应的内存地址偏移量valueOffset
* var4 需要在原value上增加的值
* @return 返回未更新前的值
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
//期待值
int var5;
do {
//获取valueOffset对应的value的值,支持volatile load
var5 = this.getIntVolatile(var1, var2);
//如果原子更新失败,则一直重试,直到成功。
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
Мы видим, что CAS может атомарно обновить только одно значение.Если мы хотим обновить несколько значений атомарно, может ли CAS это сделать?Ответ положительный.
2.4. Атомная ссылка
Если вы хотите обновить несколько значений атомарно, вам нужно использоватьAtomicReference. который используетcompareAndSwapObjectметод. Несколько значений могут быть инкапсулированы в объект, и объект может быть заменен атомарно для достижения атомарного обновления нескольких значений.
public class MultiValue {
private int value1;
private long value2;
private Integer value3;
public MultiValue(int value1, long value2, Integer value3) {
this.value1 = value1;
this.value2 = value2;
this.value3 = value3;
}
}
public class AtomicReferenceTest {
public static void main(String[] args) {
MultiValue multiValue1 = new MultiValue(1, 1, 1);
MultiValue multiValue2 = new MultiValue(2, 2, 2);
MultiValue multiValue3 = new MultiValue(3, 3, 3);
AtomicReference<MultiValue> atomicReference = new AtomicReference<>();
//因为构造AtomicReference时,没有使用有参构造函数,所以value默认值是null
atomicReference.compareAndSet(null, multiValue1);
System.out.println(atomicReference.get());
atomicReference.compareAndSet(multiValue1, multiValue2);
System.out.println(atomicReference.get());
atomicReference.compareAndSet(multiValue2, multiValue3);
System.out.println(atomicReference.get());
}
}
//输出结果
//MultiValue{value1=1, value2=1, value3=1}
//MultiValue{value1=2, value2=2, value3=2}
//MultiValue{value1=3, value2=3, value3=3}
Давайте еще раз взглянем на метод compareAndSet класса AtomicReference.
Уведомление:Все сравнения здесь используют == вместо метода equals. Поэтому лучше не предоставлять набор методов для инкапсулированного MultiValue.
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
2.5, проблема ABA CAS
Допустим, у вас на счету 100 юаней и вы хотите перевести 50 юаней на женский билет.
Мы используем CAS для атомарного обновления остатков на счетах. По какой-то причине, когда вы в первый раз нажали на перенос, произошла ошибка, вы подумали, что запрос на перенос не был инициирован, и нажали еще раз. Система открывает два потока для операций перевода.Первый поток выполняет сравнение CAS и обнаруживает, что ваш счет должен быть 100 юаней, но на самом деле имеет 100 юаней.На данный момент было переведено 50, и его нужно установить на 100 - 50 = 50 юаней, тогда баланс счета 50
Операция первого потока прошла успешно, а второй поток по какой-то причине был заблокирован, в это время ваша семья перевела вам еще 50 юаней, и перевод прошел успешно. Тогда у вас сейчас есть 100 юаней на счету;
По стечению обстоятельств разбудил второй поток и обнаружил, что на вашем счету 100 юаней, что было равно ожидаемым 100, а CAS в это время было 50. Старший брат, я так сильно плакал, посчитай, сколько у тебя денег на нужную сцену?Это проблема ABA с CAS.
public class AtomicIntegerABA {
private static AtomicInteger atomicInteger = new AtomicInteger(100);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
//线程1
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
atomicInteger.compareAndSet(100, 50);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
});
//线程2
executorService.execute(() -> {
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
atomicInteger.compareAndSet(50, 100);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
});
//线程3
executorService.execute(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
atomicInteger.compareAndSet(100, 50);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
});
executorService.shutdown();
}
}
//输出结果
//pool-1-thread-1 - 100
//pool-1-thread-1 - 50
//pool-1-thread-2 - 50
//pool-1-thread-2 - 100
//pool-1-thread-3 - 100
//pool-1-thread-3 - 50
Все думали, да ладно, не яма ли это? Это все еще работает. . . . . . . . . . . . . . успойкойся. Любая проблема, о которой вы можете подумать, может придумать jdk. Атомный пакет предоставляетAtomicStampedReference
2.6. AtomicStampedReference
Посмотрите, похоже ли имя на AtomicReference, на самом деле оно есть на AtomicReference.Добавляется номер версии, и номер версии автоматически увеличивается для каждой операции.Каждый раз CAS должен сравнивать не только значение, но и штамп, если и только если оба равны, он может быть обновлен.
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
//定义了内部静态内部类Pair,将构造函数初始化的值与版本号构造一个Pair对象。
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
//所以我们之前的value就对应为现在的pair
private volatile Pair<V> pair;
Давайте взглянем на его метод CAS.
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
//只有在旧值与旧版本号都相同的时候才会更新为新值,新版本号
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
Или пример передачи выше, мы используем AtomicStampedReference, чтобы увидеть, разрешена ли она.
public class AtomicStampedReferenceABA {
/**
* 初始化账户中有100块钱,版本号对应0
*/
private static AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(100, 0);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
int[] result = new int[1];
//线程1
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
//将100更新为50,版本号+1
atomicInteger.compareAndSet(100, 50, 0, 1);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
});
//线程2
executorService.execute(() -> {
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
//将50更新为100,版本号+1
atomicInteger.compareAndSet(50, 100, 1, 2);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
});
//线程3
executorService.execute(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
//此线程还是以为没有其他线程进行过更改,所以旧版本号还是0
atomicInteger.compareAndSet(100, 50, 0, 1);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
});
executorService.shutdown();
}
}
//输出结果
//pool-1-thread-1 - 100
//pool-1-thread-1 - 50
//pool-1-thread-2 - 50
//pool-1-thread-2 - 100
//pool-1-thread-3 - 100
//pool-1-thread-3 - 100
Маме больше не нужно беспокоиться о моем безденежье.
3. Резюме
В этой статье подробно объясняется принцип CAS. CAS может атомарно обновлять значение (включая объекты), что в основном используется в сценариях, где больше читается и меньше пишется, например, в операциях атомарного автоинкремента. Если вызывается несколькими потоками, после CAS произойдет сбой, произойдет бесконечный цикл. Повторяйте попытку, пока обновление не будет успешным. Эта ситуация сильно загружает ЦП, и, хотя блокировки нет, прокрутка цикла может быть дороже, чем блокировка. В то же время есть проблема ABA, но AtomicStampedReference решается добавлением механизма номера версии. На самом деле для пакета atomic добавлен jdk1.8LongAdder, эффективность выше, чем у AtomicLong, 9 драконов еще не ступала нога и обязательно будет продуктом в будущем. Пакет J.U.C (java.util.concurrent) использует много CAS, и ConcurrentHashMap также использует его.Если вы не понимаете CAS, как вы можете начать работу с J.U.C?
Уважаемые читатели, если вы считаете, что статья 9 Dragon была для вас полезной, пожалуйста, поставьте лайк и обратите внимание. Если воспроизводится, укажите источник.
Ссылка на ссылку: