Просматривая круг друзей несколько дней назад, я увидел абзац:Если я сейчас xx, то независимо от того, как сильно я стараюсь сейчас, я все равно буду хх, потому что мой нынешний xx определяется прошлым, и я сейчас усердно работаю, чтобы сделать мое будущее больше не хх. Если не грубое грубое обращение, если бред сейчас пытаются искать, то сразу уже не ХХ, не возможно, нужно накапливать, осадки нужно потихоньку уже не ХХ.
Ладно, куриный бульон готов.
Сегодня наш контент — это CAS, приложения с атомарными операциями и анализ исходного кода, и мы также будем использовать CAS для завершения одноэлементного шаблона, а также задействуем псевдо-совместное использование. Поскольку CAS является краеугольным камнем среды параллелизма, это очень важно, этот блог длинный, пожалуйста, будьте готовы.
Когда речь заходит о CAS, необходимо упомянуть два профессиональных термина: пессимистическая блокировка и оптимистическая блокировка. Давайте сначала посмотрим, что такое пессимистическая блокировка и что такое оптимистичная блокировка.
Пессимистический замок, оптимистичный замок
Впервые я увидел пессимистичный замок и оптимистичный замок, должно быть, когда я имел дело с интервью и просматривал вопросы интервью. Есть такой пример: как избежать модификации одной и той же записи в базе несколькими потоками.
пессимистический замок
Если это база данных mysql, используйте ключевое слово for update + транзакцию. Результатом этого является то, что когда поток A переходит к обновлению, он блокирует указанную запись, а затем поток B может только ждать. После того, как поток A изменяет данные, он фиксирует транзакцию, и блокировка снимается. B наконец может заняться своими делами. Пессимистические блокировки, как правило, исключают друг друга: войти могу только я, все остальные ждут меня. Это оказывает значительное влияние на производительность.
оптимистическая блокировка
Добавьте поле номера версии в таблицу данных: версия, это поле не требует ручного обслуживания программистом и активно поддерживается базой данных.Каждый раз, когда данные изменяются, версия будет меняться.
Когда версия теперь 1:
- Приходит поток и читает, что версия 1.
- Приходит поток B и читает, что версия 1.
- Поток выполняет операцию обновления: update stu set name='codebear', где id=1 и version=1. успех. База данных активно меняла версию на 2.
- Поток B выполнил операцию обновления: update stu set name='hello', где id=1 и version=1. Потерпеть поражение. Поскольку в настоящее время поле версии больше не равно 1.
Оптимистическую блокировку на самом деле нельзя назвать замком, у нее нет понятия блокировки.
В Java также существуют понятия пессимистических блокировок и оптимистичных блокировок, типичным представителем пессимистичных блокировок является Synchronized, а типичным представителем оптимистичных блокировок сегодня является CAS. Прежде чем говорить о CAS, давайте сначала поговорим о классе атомарных операций, поскольку CAS является краеугольным камнем класса атомарных операций, мы должны сначала рассмотреть возможности класса атомарных операций, чтобы вызвать интерес к изучению CAS.
Применение атомарных операций
Давайте сначала рассмотрим применение класса атомарных операций. В Java предусмотрено множество классов атомарных операций, таких как AtomicInteger, который имеет метод автоинкремента.
public class Main {
public static void main(String[] args) {
Thread[] threads = new Thread[20];
AtomicInteger atomicInteger = new AtomicInteger();
for (int i = 0; i < 20; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
atomicInteger.incrementAndGet();
}
});
threads[i].start();
}
join(threads);
System.out.println("x=" + atomicInteger.get());
}
private static void join(Thread[] threads) {
for (int i = 0; i < 20; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
результат операции:
В этом и заключается магия класса атомарных операций.В случае высокого параллелизма этот метод будет иметь больше преимуществ, чем Synchronized.В конце концов, ключевое слово Synchronized сериализует код и потеряет преимущество многопоточности.
Давайте рассмотрим другой случай:
Если есть требование, начальное значение поля равно 0, и открываются три потока:
- Выполнение одного потока: когда x=0, x изменяется на 100
- Выполнение одного потока: когда x=100, x изменяется на 50
- Выполнение одного потока: когда x=50, x изменяется на 60
public static void main(String[] args) {
AtomicInteger atomicInteger=new AtomicInteger();
new Thread(() -> {
if(!atomicInteger.compareAndSet(0,100)){
System.out.println("0-100:失败");
}
}).start();
new Thread(() -> {
try {
Thread.sleep(500);////注意这里睡了一会儿,目的是让第三个线程先执行判断的操作,从而让第三个线程修改失败
} catch (InterruptedException e) {
e.printStackTrace();
}
if(!atomicInteger.compareAndSet(100,50)){
System.out.println("100-50:失败");
}
}).start();
new Thread(() -> {
if(!atomicInteger.compareAndSet(50,60)){
System.out.println("50-60:失败");
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Запускаем результат тот же:
Этот пример вроде ничего не значит, даже немного скучноватый, зачем приводить этот пример, потому что здесь вызывается метод compareAndSet, первая буква CAS, и передает два параметра, эти два параметра в нативном CAS Что должна быть передана в операции ближе к родной операции CAS.
Поскольку класс атомарных операций такой замечательный, нам необходимо изучить краеугольный камень класса атомарных операций: CAS.
CAS
Полное название CAS — Compare And Swap, то есть сравнение и обмен.Конечно, можно сказать и по-другому: Compare And Set, вызов нативной операции CAS должен определить три значения:
- поле для обновления
- Ожидаемое значение
- Новое значение
Среди них обновляемое поле (переменная) иногда разбивается на два параметра: 1. Экземпляр 2. Адрес смещения.
Может быть, когда вы это увидите, вы почувствуете себя облачно и туманно, и вы не знаете, о чем я говорю. Это не имеет значения, продолжайте стиснуть зубы.
Давайте сначала посмотрим на исходный код compareAndSet.
Анализ исходного кода compareAndSet
Прежде всего, при вызове этого метода необходимо передать два параметра: один — ожидаемое значение, другой — новое значение, ожидаемое значение — старое значение (значение, а не поле), а новое значение — значение мы хотим изменить (значение, а не поле). Давайте посмотрим на внутреннюю реализацию этого метода:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
Вызывается метод compareAndSwapInt в unsafe. В дополнение к двум параметрам, которые мы передали этому методу, передаются два параметра. Эти два параметра — адрес экземпляра и адрес смещения, о которых я упоминал выше, и это представляет текущий класс. Экземпляр класса AtomicInteger. Что, черт возьми, это за адрес смещения? Проще говоря, он определяет, где в экземпляре находится поле, которое нам нужно изменить. Зная экземпляр и зная, где в экземпляре находится поле, которое нам нужно изменить, мы можем определить это поле. Однако этот процесс определения выполняется не в Java, а на более низком уровне.
Адрес смещения получается в статическом блоке кода этого класса:
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
unsafe.objectFieldOffset получает параметр типа Поле, и получает адрес смещения соответствующего поля, вот адрес смещения поля значения в этом классе, то есть AtomicInteger.
Давайте посмотрим на определение поля значения:
private volatile int value;
Volatile гарантирует видимость памяти.
Вы наверняка захотите узнать, что делают методы compareAndSwapInt и objectFieldOffset.К сожалению, мой личный уровень ограничен, и я пока не смог его изучить.Знаю только, что этот метод написания - JNI, который вызовет C или C++ , и в конечном итоге отправит соответствующую инструкцию в ЦП, что гарантированоатомарностьиз.
Мы можем посмотреть на определения этих двух методов:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public native long objectFieldOffset(Field var1);
Эти два метода помечены как native.
Сделаем более наглядное объяснение метода compareAndSwapInt:
Когда мы выполним метод compareAndSwapInt и передадим 10 и 100, Java будет общаться с нижним уровнем: старое железо, я дал вам экземпляр и адрес смещения поля, вы можете помочь мне увидеть, равно ли значение этого поля 10 , если это 10, вы можете изменить его на 100 и вернуть true, если нет, не изменяйте его и верните false.
Процесс сравнения называется сравнением, а процесс изменения значения — обменом, поскольку старое значение заменяется новым значением, поэтому мы называем эту операцию CAS.
Давайте посмотрим на исходный код incrementAndGet.
Анализ исходного кода incrementAndGet
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
Метод incrementAndGet будет вызываться в методе getAndAddInt, который имеет три параметра:
- var1: экземпляр.
- var2: адрес смещения.
- var4: значение, которое нужно увеличить, здесь 1.
Внутри метода getAndAddInt есть цикл while.Внутри тела цикла получается соответствующее значение в соответствии с экземпляром и адресом смещения, который сначала вызывается A, а затем в то время просматривается содержимое суждения.JDK связывается с нижний слой: эй, я помещаю экземпляр и вам дается адрес смещения, вы можете помочь мне увидеть, является ли это значение A, если да, помогите мне изменить его на A + 1, вернуть true, если нет, вернуть false .
Вот вопрос для размышления: зачем нужен цикл while?
Например, два потока выполняют метод getIntVolatile одновременно, и полученное значение равно 10. Среди них поток A выполняет собственный метод, и модификация прошла успешно, но поток B не может его изменить, поскольку операция CAS может гарантировать атомарность, поэтому поток B может только усердно работать для повторного цикла.На этот раз полученное значение равно 11, собственный метод выполняется снова, и модификация прошла успешно.
Такой цикл while имеет причудливое имя:CAS спин.
Давайте представим, если параллелизм сейчас действительно высок, что произойдет? Большое количество потоков выполняет вращения CAS, что приводит к пустой трате ресурсов ЦП. Итак, после Java8 в классы атомарных операций были внесены определенные оптимизации, о которых мы поговорим позже.
Может быть, вы все еще не понимаете лежащую в основе реализацию класса атомарных операций, и вы до сих пор не знаете, что означают методы ниже небезопасных.В конце концов, я просто сейчас прочитал код, как говорится "На бумаге я чувствую себя поверхностным в конце, и я абсолютно точно знаю, что это дело должно быть сделано.", поэтому нам нужно назвать приведенный ниже метод небезопасным, чтобы углубить наше понимание.
Unsafe
Небезопасный: Небезопасный, раз есть такое название, значит, этот класс относительно опасен.Официальные лица Java не рекомендуют нам напрямую оперировать классом Небезопасный, но ведь это этап обучения, просто пишите демки, пока он не выпущен в производственную среду, какое это имеет значение?
Ниже Unsafe есть еще много методов. Мы выбираем несколько методов для просмотра, и, наконец, мы будем использовать эти методы для завершения демонстрации.
objectFieldOffset: получает данные типа Field и возвращает адрес смещения. compareAndSwapInt: сравнить своп, получить четыре параметра: экземпляр, адрес смещения, ожидаемое значение, новое значение. getIntVolatile: получить значение, поддержать Volatile, получить два параметра: экземпляр, адрес смещения.
Эти три метода уже фигурировали в анализе исходного кода выше, и они также были в определенной степени объяснены.Я объясню их здесь, чтобы углубить свое впечатление.Когда я изучал CAS, я также неоднократно читал блоги и исходный код.Внезапно осуществленный. Нам нужно использовать эти три метода для завершения демонстрации: написать метод автоинкремента атомарной операции, значение автоинкремента можно настроить, да, я уже проанализировал этот метод выше. Код выпущен непосредственно ниже:
public class MyAtomicInteger {
private volatile int value;
private static long offset;//偏移地址
private static Unsafe unsafe;
static {
try {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
unsafe = (Unsafe) theUnsafeField.get(null);
Field field = MyAtomicInteger.class.getDeclaredField("value");
offset = unsafe.objectFieldOffset(field);//获得偏移地址
} catch (Exception e) {
e.printStackTrace();
}
}
public void increment(int num) {
int tempValue;
do {
tempValue = unsafe.getIntVolatile(this, offset);//拿到值
} while (!unsafe.compareAndSwapInt(this, offset, tempValue, value + num));//CAS自旋
}
public int get() {
return value;
}
}
public class Main {
public static void main(String[] args) {
Thread[] threads = new Thread[20];
MyAtomicInteger atomicInteger = new MyAtomicInteger();
for (int i = 0; i < 20; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
atomicInteger.increment(1);
}
});
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("x=" + atomicInteger.get());
}
}
результат операции:
У вас могут возникнуть сомнения, почему вам нужно использовать отражение, чтобы получить небезопасный. На самом деле, это для защиты нас от JDK, чтобы мы не могли легко получить небезопасный. Если мы получим небезопасный, как JDK, будет сообщено об ошибке:
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");//如果我们也以getUnsafe来获得theUnsafe,会抛出异常
} else {
return theUnsafe;
}
}
CAS и одноэлементный шаблон
Да, вы правильно прочитали, и я не ошибся.Вы также можете использовать CAS для завершения режима singleton.Хотя в обычной разработке никто не будет использовать CAS для завершения режима singleton, но это хороший способ проверьте, изучили ли вы тему CAS.
public class Singleton {
private Singleton() {
}
private static AtomicReference<Singleton> singletonAtomicReference = new AtomicReference<>();
public static Singleton getInstance() {
while (true) {
Singleton singleton = singletonAtomicReference.get();// 获得singleton
if (singleton != null) {// 如果singleton不为空,就返回singleton
return singleton;
}
// 如果singleton为空,创建一个singleton
singleton = new Singleton();
// CAS操作,预期值是NULL,新值是singleton
// 如果成功,返回singleton
// 如果失败,进入第二次循环,singletonAtomicReference.get()就不会为空了
if (singletonAtomicReference.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
Аннотации были написаны четко, поэтому вы можете просмотреть аннотации для лучшего понимания.
ABA
Метод compareAndSet, демка была написана выше, так же можно попробовать проанализировать исходный код, больше разбирать не буду, причина, по которой хочу еще раз упомянуть метод compareAndSet, это поднятие вопроса.
Предположим, есть три шага:
- Изменить 150 на 50
- Изменить 50 на 150
- Изменить 150 на 90
Пожалуйста, посмотрите внимательно, что делают эти три шага: переменная вначале равна 150, изменена на 50, а позже изменена на 150! (снова измените его) и, наконец, если эта переменная равна 150, измените ее на 90. Это проблема с ABA в CAS.
Третий шаг — определить, равно ли значение 150. Есть два разных требования:
- Верно, хотя это значение было изменено, теперь оно было изменено обратно, поэтому решение третьего шага справедливо.
- Нет, хотя это значение равно 150, это значение было изменено, поэтому решение третьего шага недействительно.
Для второго требования мы можем использовать AtomicStampedReference для решения этой проблемы, AtomicStampedReference поддерживает дженерики, которые имеют концепцию штампа. Вставьте код прямо ниже:
public static void main(String[] args) {
try {
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(150, 0);
Thread thread1 = new Thread(() -> {
Integer oldValue = atomicStampedReference.getReference();
int stamp = atomicStampedReference.getStamp();
if (atomicStampedReference.compareAndSet(oldValue, 50, 0, stamp + 1)) {
System.out.println("150->50 成功:" + (stamp + 1));
}
});
thread1.start();
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(1000);//睡一会儿,是为了保证线程1 执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
Integer oldValue = atomicStampedReference.getReference();
int stamp = atomicStampedReference.getStamp();
if (atomicStampedReference.compareAndSet(oldValue, 150, stamp, stamp + 1)) {
System.out.println("50->150 成功:" + (stamp + 1));
}
});
thread2.start();
Thread thread3 = new Thread(() -> {
try {
Thread.sleep(2000);//睡一会儿,是为了保证线程1,线程2 执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
Integer oldValue = atomicStampedReference.getReference();
int stamp = atomicStampedReference.getStamp();
if (atomicStampedReference.compareAndSet(oldValue, 90, 0, stamp + 1)) {
System.out.println("150->90 成功:" + (stamp + 1));
}
});
thread3.start();
thread1.join();
thread2.join();
thread3.join();
System.out.println("现在的值是" + atomicStampedReference.getReference() + ";stamp是" + atomicStampedReference.getStamp());
} catch (Exception e) {
e.printStackTrace();
}
}
Оптимизация Java8 для классов атомарных операций
При парсинге исходного кода incrementAndGet возникает проблема: при высоком параллелизме N мультипотоков конкурируют за одно и то же поле, вращаясь, что, несомненно, окажет определенную нагрузку на ЦП, поэтому в Java8 он обеспечивает более полную атомарную операцию класс: LongAdder.
Кратко расскажем о том, какую оптимизацию он сделал.Он внутренне поддерживает массив Cell[] и базу, а Cell поддерживает значение.При наличии конкуренции JDK выберет Cell в соответствии с алгоритмом и проведет анализ на значение в нем.Операция, если конкуренция все еще существует, она попытается снова с другой ячейкой и, наконец, добавит значение в ячейке [] к базе, чтобы получить окончательный результат.
Поскольку код более сложный, я выбрал несколько наиболее важных вопросов с вопросами, чтобы увидеть исходный код:
- Когда Cell[] инициализируется.
- Если не будет конкуренции, будет эксплуатироваться только база, на что посмотреть.
- Каковы правила инициализации Cell[].
- Каковы сроки расширения Cell[].
- Как инициализировать Cell[] и расширить Cell[], чтобы обеспечить потокобезопасность.
Вот UML-диаграмма класса LongAdder:
Добавить метод:
public void add(long x) {
Cell[] cs; long b, v; int m; Cell c;
if ((cs = cells) != null || !casBase(b = base, b + x)) {//第一行
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 ||//第二行
(c = cs[getProbe() & m]) == null ||//第三行
!(uncontended = c.cas(v = c.value, v + x)))//第四行
longAccumulate(x, null, uncontended);//第五行
}
}
Первая строка: || Судья, первый должен судить, является ли cs=cells [не пустым], а второй должен судить, является ли CAS [неудачным]. Что делает casBase?
final boolean casBase(long cmp, long val) {
return BASE.compareAndSet(this, cmp, val);
}
Это относительно просто, просто вызовите метод compareAndSet, чтобы определить, успешно ли он выполнен:
- Возвращает true, если текущего соревнования нет.
- Если в настоящее время существует конфликт, поток вернет false.
Вернитесь к первой строке и объясните решение в целом: если ячейка [] была инициализирована или есть конкуренция, будет введена вторая строка кода. Если нет конкуренции и инициализации, он не войдет во вторую строку кода.
Это отвечает на второй вопрос: если не будет конкуренции, будет работать только база, которую отсюда видно.
Вторая строка кода: || Суд, первое определяет, является ли CS NULL, определяется последнее (длина CS -1) больше [0]. Анализируя эти два, он определяется клетку [] ли инициализация. Если не инициализирован, войдете в пятую строку кода.
Третья строка кода: Если ячейка инициализирована, получите число с помощью алгоритма [getProbe() & m], определите, является ли cs[number] [NULL], и назначьте cs[number] для c, если оно [NULL], оно войдет первые пять строк кода. Нам нужно кратко взглянуть на то, что делается в getProbe():
static final int getProbe() {
return (int) THREAD_PROBE.get(Thread.currentThread());
}
private static final VarHandle THREAD_PROBE;
Нам нужно только знать, что этот алгоритм рассчитывается на основе THREAD_PROBE.
Четвертая строка кода: Операция CAS выполняется на c, чтобы убедиться, что она успешна, и возвращаемое значение присваивается uncontended.Если нет конкуренции, она будет успешной.Если в настоящее время есть конкуренция, она потерпит неудачу.Существует !() снаружи , поэтому CAS не работает, будет введена пятая строка кода. Следует отметить, что здесь операция уже выполняется над элементом Cell.
Пятая строка кода: Этот метод очень сложен внутренне, давайте сначала рассмотрим весь метод:
Есть три если: 1. Определите, инициализированы ли ячейки, и если да, введите это, если.
В нем есть еще 6 if, что пугает, но здесь нам не нужно обращать внимание на все из них, потому что наша цель - решить проблемы, поднятые выше.
Сначала общий вид:
Первое определение: алгоритм придумать элемент CS [] в и присвоен C, а затем определить, следует ли нулю [], если [] нулевой, в это, если.
if (cellsBusy == 0) { // 如果cellsBusy==0,代表现在“不忙”,进入这个if
Cell r = new Cell(x); //创建一个Cell
if (cellsBusy == 0 && casCellsBusy()) {//再次判断cellsBusy ==0,加锁,这样只有一个线程可以进入这个if
//把创建出来Cell元素加入到Cell[]
try {
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
break done;
}
} finally {
cellsBusy = 0;//代表现在“不忙”
}
continue; // Slot is now non-empty
}
}
collide = false;
Это дополняет первый вопрос.При инициализации Cell[] один из элементов равен NULL.Здесь инициализируется тот элемент, который равен NULL, то есть инициализируется только при использовании этого элемента.
Шестое суждение: оцените, является ли CellBusy равным 0, и заблокируйте его.Если это успешно, введите это, если и расширьте емкость Cell[].
try {
if (cells == cs) // Expand table unless stale
cells = Arrays.copyOf(cs, n << 1);
} finally {
cellsBusy = 0;
}
collide = false;
continue;
Это отвечает на половину пятого вопроса: при расширении Cell[] CAS используется для добавления блокировки, поэтому гарантируется безопасность потока.
Так что насчет четвертого вопроса? Прежде всего, вы должны отметить, что самый внешний цикл — это бесконечный цикл for (;;), который завершается только после разрыва.
В начале столкновение ложно. В третьем if над ячейкой выполняется операция CAS. В случае успеха она сломается, поэтому нам нужно предположить, что это сбой, и ввести четвертое if, которое будет судить о ячейке. Ячейка [[] больше, чем количество ядер ЦП, если меньше, чем количество ядер, она войдет в пятое суждение. В это время, если столкновение ложно, оно войдет в это, если изменить столкновение на истину, что означает наличие конфликта, а затем запустите метод advanceProbe, чтобы сгенерировать новый цикл THREAD_PROBE, снова. Если CAS по-прежнему терпит неудачу в третьем случае, оцените еще раз, превышает ли длина Cell[] количество ядер. Если она меньше, чем количество ядер, она войдет в пятое суждение. В это время столкновение верно , поэтому он не войдет в пятый, если таким образом будет введен шестой суд и осуществлено расширение. Это очень сложно.
2. Первые два суждения легко понять, но третье суждение в основном:
final boolean casCellsBusy() {
return CELLSBUSY.compareAndSet(this, 0, 1);
}
cas устанавливает CELLSBUSY в 1, что можно понимать как добавление блокировки, потому что вот-вот произойдет инициализация.
try { // Initialize table
if (cells == cs) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
break done;
}
} finally {
cellsBusy = 0;
}
Initialize Cell[], видно, что длина равна 2. По алгоритму инициализируем один из элементов, то есть длина Cell[] в этот момент равна 2, но в ней есть элемент, который еще NULL, и теперь выполняется только один из элементов.После инициализации CellBusy окончательно меняется на 0, что означает, что сейчас он "не занят".
Это ответы Первая проблема: когда есть конкуренция и Cell[] не инициализирован, Cell[] будет инициализирован. Четвертый вопрос: Правило инициализации состоит в том, чтобы создать массив длины 2, но только один из элементов будет инициализирован, а другой элемент будет NULL. Половина пятого вопроса: когда Cell[] инициализируется, CAS используется для добавления блокировки, поэтому можно гарантировать потокобезопасность.
3. Если вышеуказанное не помогло, выполните операцию CAS на базе.
Если вы следуете за мной, чтобы посмотреть на исходный код, вы найдете комментарий, который, возможно, никогда не видел раньше:
Для чего эта аннотация?Contended используется для устранения ложного обмена.
Ну, еще одно слепое пятно знаний, что такое псевдо-обмен.
ложный обмен
Мы знаем отношения между ЦП и памятью: когда процессор нуждается в данных, вы пойдете в кэш. Если нет, если нет, он пойдет в память, найдите его, скопируйте данные в кэш, перейдите непосредственно к кэше вывести его.
Но этот аргумент не идеален, данные в кеше на основестрока кэшахранится в форме, что это значит? То есть в строке кэша может быть несколько данных. Если размер строки кэша составляет 64 байта, ЦП извлекает данные из памяти, выбирает соседние 64 байта данных и копирует их в кэш.
Это оптимизация для одного потока. Только представьте, если процессору нужны данные A, он извлекает соседние данные BCDE из памяти и помещает их в кеш.Если процессору снова нужны данные BCDE, он может обратиться непосредственно к кешу, чтобы получить их.
Но при многопоточности есть недостаток, потому что данные одной и той же строки кэша могут одновременно считываться только одним потоком, который называетсяложный обмен.
Есть ли способ решить эту проблему? Умный разработчик придумал решение: если размер строки кэша 64 байта, я могу добавить несколько избыточных полей, чтобы дополнить ее до 64 байт.
Например, мне нужно только поле длинного типа.Теперь я добавлю 6 полей длинного типа в качестве заливки, длинный счет на 8 байт, теперь поле 7 длинных типов, то есть 56 байт, и другие заголовки объекта 8 байт, всего 64 байта, достаточно для кэширования строк.
Но этот метод недостаточно элегантен, поэтому аннотация @jdk.internal.vm.annotation.Contended была введена в Java8 для решения проблемы ложного совместного использования. Но если разработчики хотят использовать эту аннотацию, им нужно добавить параметры JVM, я не буду называть здесь конкретные параметры, потому что сам их не тестировал.
Эта глава довольно длинная и охватывает почти большинство общих проблем в CAS.
Фреймворк параллелизма очень сложен для изучения, потому что знания о параллелизме редко используются в разработке, но параллелизм — очень эффективное средство повышения производительности и пропускной способности программы, поэтому параллелизму стоит потратить время на изучение.