«У вас сегодня было собеседование» — блокировки и модели памяти для параллельного программирования

Java

предисловие

Один из наиболее часто задаваемых вопросов в интервью — это распределение, а другой — параллелизм. И материал в JUC (java.util.concurrent) является краеугольным камнем параллельного программирования. Прошло некоторое время с момента последнего интервью, и, усердно работая, я также нашел время, чтобы подготовиться к части параллельного программирования на java. Сегодня в расслабленном и радостном настроении я снова отправился на собеседование на крупную фабрику.

Сессия интервью

  • Интервьюер: Сначала скажите мне, что вы правы.synchronizedпонимание.

  • Я: синхронизация может гарантировать, что только один метод может войти в критическую секцию одновременно, когда метод или код выполняется, а также может обеспечить видимость в памяти общих переменных.

  • Я: Каждый объект в Java можно использовать в качестве блокировки, что является основой для синхронизированного для достижения синхронизации: 1. Обычный метод синхронизации: блокировкой является текущий экземпляр объекта.
    2. Статический метод синхронизации, замок является объектом класса текущего класса.
    3. Синхронизированный кодовый блок: Блокировка — это объект в круглых скобках.

  • Интервьюер: Когда поток обращается к синхронизированному блоку кода, он должен сначала получить блокировку для выполнения кода и выйти или выдать исключение, чтобы снять блокировку Как это реализовано?

  • Я: Синхронизированные блоки кода реализованы с помощью директив monitorenter и monitorexit, а синхронизированные методы реализованы с помощью ACCSYNCHRONIZED в модификаторе метода.
    1. Блок синхронного кода: инструкция monitorenter вставляется в начало кода синхронизации, а инструкция monitorexit вставляется в конец блока кода синхронизации JVM гарантирует, что каждый монитор существует Мониторэнтер соответствует этому. Любая переписка имеет связанный с ней монитор, и когда монитор удерживается, он будет заблокирован. Поток выполняет команду monitorenter , он попытается получить право собственности на монитор, соответствующее объекту, то есть попытается получить блокировку объекта.
    2. Метод синхронизации: синхронизированный метод заключается в установке позиции флага синхронизации в поле accessflags метода на 1 в таблице методов файла класса, что указывает на то, что метод является синхронизированным методом. И используйте объект, который вызывает метод, или класс, к которому принадлежит метод, для представления Klass в качестве объекта блокировки во внутреннем объекте JVM.

  • Интервьюер: Вы только что упомянули, что каждому объекту соответствует соответствующий монитор, так что же такое монитор?

  • Я: Мы можем понимать это как инструмент синхронизации или как механизм синхронизации, который обычно описывается как объект. Как и все является объектом, все Java-объекты рождаются мониторами, Каждый Java-объект потенциально может стать монитором, потому что в дизайне Java каждый Java-объект имеет невидимую блокировку с момента своего рождения, которая называется внутренней блокировкой или блокировкой монитора.

  • Я: (продолжая) Монитор — это структура данных, приватная для потока, у каждого потока есть список доступных записей монитора, а также есть глобальный список доступных мониторов. каждый заблокированный объект будет связан с монитором (LockWord в MarkWord заголовка объекта указывает на начальный адрес монитора), а поле Owner в мониторе хранит уникальный идентификатор потока, которому принадлежит блокировка. Указывает, что блокировка занята этим потоком.

  • Интервьюер: Очень хорошо. Мы знаем, что синхронизация — это пессимистичная блокировка, и она всегда считалась тяжеловесной блокировкой. Но jdk1.6 оптимизирует блокировку, напримерСпин-блокировки, адаптивные спин-блокировки, устранение блокировок, блокировки смещения и Легкий замоки другие технологии для снижения накладных расходов на операции блокировки, вы все это понимаете?

  • Я: Я знаю некоторых. Существует четыре основных состояния блокировки: состояние без блокировки, состояние смещенной блокировки, облегченное состояние блокировки и тяжелое состояние блокировки. Они будут постепенно обостряться по мере того, как конкуренция будет становиться все более жесткой. Обратите внимание, что замки могут быть обновлены, но не понижены. Эта стратегия предназначена для повышения эффективности получения и освобождения блокировок.

  • Интервьюер: Тогда скажи мне сначалаблокировка спина

  • Я: Для блокировки и пробуждения потоков требуется, чтобы ЦП переключался из состояния пользователя в состояние ядра. Частая блокировка и пробуждение являются большой нагрузкой для ЦП и в то же время влияют на возможности параллелизма в системе. В то же время , мы обнаружили, что многие приложения Состояние блокировки блокировки объекта будет длиться только в течение короткого периода времени.Не стоит часто блокировать и пробуждать потоки на этот короткий период времени, поэтому вводятся спин-блокировки. Что такое спин-блокировка - это позволить потоку подождать некоторое время и не будет Немедленно приостановите выполнение, чтобы увидеть, не освободит ли блокировку поток, удерживающий блокировку, в ближайшее время. Так вот вопрос, сколько ждать? Если времени мало, то поток, удерживающий блокировку, не сможет снять блокировку, если время велико, то он отнимет время процессора Типичный пример — «занять яму, не нагадив». Наоборот, это приводит к потере производительности. Следовательно, должно быть ограничение на количество ожиданий вращения (вращение), и если вращение превышает определенное время и все еще не получает блокировку, оно будет приостановлено.

  • Интервьюер: Я помнюадаптивная спин-блокировка, умнее. Можешь мне ответить?

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

  • Интервьюер: Покажите следующий код, как вы думаете, будет ли операция блокировки?

public static void main(String [] args) {
        Vector<String> vector = new Vector<>();
        for (int i=0; i<10; i++) {
            vector.add(i+"");
        }
        System.out.println(vector);
    }
  • Я не буду. В этом случае JVM обнаруживает, что вероятность гонки за общими данными отсутствует, и JVM выполняет эти блокировки синхронизации.снятие блокировки. Основой устранения блокировок является поддержка данных для анализа побегов.

  • Интервьюер: Посмотрите на другой фрагмент кода и проанализируйте, где он заблокирован?

public static void test() {
        List<String> list = new ArrayList<>();
        for (int i=0; i<10; i++) {
            synchronized (Demo.class) {
                list.add(i + "");
            }
        }
        System.out.println(list);
    }
  • I: Хотя Synchronized находится в цикле, на самом деле он расширен до цикла.замок огрублениеЭто соединение нескольких последовательных операций блокировки и разблокировки вместе для расширения в более крупный замок.

  • Интервьюер: Можете ли вы сказать мнеЛегкий замок?

  • Я: Основой облегченных блокировок для улучшения производительности синхронизации программ является то, что для большинства блокировок нет конкуренции (в отличие от предвзятых блокировок) на протяжении всего цикла синхронизации Это эмпирические данные. Если конфликтов нет, облегченные блокировки используют операции CAS, чтобы избежать использования мьютекса. Однако при наличии состязания помимо накладных расходов мьютекса происходит дополнительная операция CAS, поэтому в случае состязания облегченные блокировки работают медленнее, чем традиционные тяжеловесные блокировки.

  • Далее я сказал: процесс блокировки легких замков: 1. Когда код входит в блок синхронизации, если состояние блокировки объекта синхронизации является состоянием без блокировки (флаг блокировки равен «01», независимо от того, является ли это смещенной блокировкой, равен «0»), виртуальная машина сначала установить кадр стека текущего потока Пространство с именем Lock Record используется для хранения копии текущего слова метки объекта, официально называемого словом Displaced Mark Word. В это время состояние стека потока и заголовка объекта показано на рисунке:
    Легкая процедура блокировки блокировки 1.png

2. Скопируйте Mark Word из заголовка объекта в Lock Record.
3. После успешного копирования виртуальная машина будет использовать операцию CAS, чтобы попытаться обновить слово метки объекта блокировки до указателя на запись блокировки и указать указатель владельца в записи блокировки в кадре стека потоков на слово маркировки объекта. если это обновление Если действие выполнено успешно, поток владеет блокировкой объекта, а флаг блокировки объекта Mark Word устанавливается на «00», указывая на то, что объект находится в состоянии облегченной блокировки.

4. Если операция обновления не удалась, виртуальная машина сначала проверит, указывает ли Mark Word объекта на фрейм стека текущего потока.Если это так, это означает, что текущий поток уже владеет блокировкой объекта, и он может напрямую войти в блок синхронизации, чтобы продолжить выполнение. В противном случае объясните Когда несколько потоков конкурируют за блокировки, облегченная блокировка преобразуется в тяжеловесную блокировку, а значение состояния флага блокировки становится равным 10. Указатель на тяжеловесную блокировку (мьютекс) сохраняется в слове метки и ожидает позже. Поток замка также входит в состояние блокировки.

  • Интервьюер: Очень подробно. Вы можете объяснитьБлокировка смещения?

  • Я: Целью предвзятых замков - это устранение примитивов синхронизации данных при отсутствии конкуренции и дальнейшее повышение производительности программы. Предварительные замки будут смещены в сторону первого потока, которая его приобретает. Если блокировка не приобретается другими потоками в последующем процессе выполнения, то замок удерживается Блокированные нити никогда не понадобится синхронизации.

  • Я сделал паузу, а затем сказал: Когда поток получает блокировку в первый раз, поток использует операцию CAS для записи идентификатора потока в объекте Mark Word и устанавливает флаг смещения в 1. Позже поток входит и выходит из кода При блокировке не требуются CAS-операции Чтобы заблокировать и разблокировать, просто проверьте, хранит ли слово Mark в заголовке объекта идентификатор, указывающий на текущий поток. Если проверка прошла успешно, поток получил блокировку. Режим смещения завершается, когда другой поток пытается получить блокировку. В зависимости от того, находится ли объект блокировки в настоящее время в заблокированном состоянии, смещение меняется на противоположное и восстанавливается в разблокированное или облегченное заблокированное состояние.

  • Интервьюер:В чем разница между смещенными замками, облегченными замками и тяжелыми замками??

  • Я: Смещенные замки и легкие замки — это оптимистичные замки, а тяжелые замки — пессимистичные замки. Когда объект создается впервые и к нему не обращается ни один поток, он смещается, то есть он думает, что только один поток может получить к нему доступ, поэтому, когда первый поток При обращении к нему он смещается в сторону этого потока, и в это время объект удерживает смещенную блокировку. Смещенный к первому потоку, этот поток использует операцию CAS при изменении заголовка объекта, чтобы он стал предвзятой блокировкой, и изменяет ThreadID в заголовке объекта на свой собственный идентификатор, а затем ему нужно только сравнить идентификатор для доступа к объекту. Как только второй поток получает доступ к этому объекту, поскольку смещенная блокировка не будет снята, второй поток видит, что объект смещен, что указывает на наличие конкуренции на этом объекте, и проверяет, жив ли еще поток, который первоначально удерживал объект. Если он зависает, объект можно сделать свободным от блокировки, а затем перенаправить на новый поток. Если исходный поток все еще жив, стек операций этого потока выполняется немедленно и проверяется использование объекта. в настоящее время модернизирован до легкого замка). Если нет никакого использования, объект может быть восстановлен в свободное от блокировки состояние, а затем перенаправлен.

  • Я: (продолжая) Облегченные замки считают, что конкуренция существует, но степень конкуренции очень незначительна.Как правило, два потока смещают работу одного и того же замка или вращаются, а другой поток освобождает блокировку. Но когда спин превышает определенное количество раз, или нить держит замок, Поток вращается, и есть третий визит.Легкая блокировка расширяется в тяжеловесную блокировку.Тяжеловесная блокировка блокирует все потоки, кроме потока, которому принадлежит блокировка, предотвращая простаивание ЦП.Проще говоря: есть конкуренция, диагональный замок модернизируется до облегченного замка, и конкуренция постепенно становится жесткой. Легкие замки модернизируются до тяжелых замков.

  • Интервьюер: Вы понимаете модель памяти Java? Можете ли вы рассказать мне, как вы понимаете JMM?

  • Я: В стандарте JSR113 есть краткое введение в JMM: Виртуальная машина Java поддерживает многопоточное выполнение. В Java класс Thread представляет поток. Единственный способ создать поток — создать объект-экземпляр класса Thread.При вызове метода запуска объекта будет выполнен соответствующий поток. Поведение потоков иногда противоречит нашей интуиции, особенно если потоки не синхронизированы должным образом. Эта спецификация описывает семантику многопоточных программ на платформе JMM, в частности, когда запись потока в общую переменную может быть видна другим потокам. Это официальное введение порядка.

  • Я:Модель памяти Java — это воплощение модели памяти в JVM. Основная цель этой модели — определить правила доступа для каждой общей переменной в программе, то есть низкоуровневые детали хранения и извлечения переменных в памяти и из памяти виртуальной машины. Эти правила используются для стандартизации операций чтения и записи в память, обеспечивая видимость, атомарность и упорядоченность в параллельных сценариях.JMM предусматривает, что многие переменные хранятся в основной памяти, каждый поток имеет свою рабочую память, рабочая память потока сохраняет копию основной памяти, используемой в потоке, и все операции потока над переменными должны быть в Вместо чтения и записи напрямую в оперативную память. Разные потоки не могут напрямую обращаться к переменным в рабочей памяти друг друга.Передача переменных между потоками требует их собственной рабочей памяти и основной памяти. Выполните синхронизацию данных. JMM воздействует на процесс синхронизации данных между рабочей памятью и основной памятью. Он определяет, как выполнять синхронизацию данных и когда выполнять синхронизацию данных. Другими словами, связь между потоками Java контролируется моделью памяти Java, и JMM определяет, когда запись одного потока в общую переменную видна другому потоку.

  • Я: Чтобы проложить его: Multi-Threads Java обменивается через общую память, и благодаря использованию общей памяти для связи будет серия проблем, таких как атомность, видимость и упорядочение в процессе связи. JMM - решить эти проблемы, Эта модель устанавливает некоторые спецификации для обеспечения атомности, видимости и упорядочения чтения и пишет на общие переменные в контексте многоядерного многопоточного программирования CPU.

  • Интервьюер: Тогда вы сказали, что модель памяти Javahappens-beforeправило?

  • Я: В JMM, если результат выполнения одной операции должен быть виден другой, между этими двумя операциями должна быть связь «происходит до». Принцип «происходит раньше» — очень важный принцип в JMM, он является основным основанием для оценки наличия конкуренции в данных и безопасности потока, а также обеспечивает видимость в многопоточной среде. Ниже я расскажу о содержании бывало-прежде: Принцип «случается раньше» определяется следующим образом:
    1. Если операция происходит до другой операции, то результат выполнения первой операции будет виден второй операции, а порядок выполнения первой операции - до второй операции.
    2. Между двумя операциями существует отношение «происходит до», что не обязательно означает, что они должны выполняться в порядке, сформулированном принципом «произошло до». Если результат выполнения после переупорядочивания согласуется с результатом выполнения в соответствии с отношением «происходит до», то такой вид переупорядочивания не является недопустимым.
    Ниже приведены принципы «происходит до»:
    1. Правила заказа программ: в потоке, в зависимости от порядка записи кода, операции, написанные на фронте, происходят первыми до операций, написанных на спине.
    2. Правила блокировки: сначала выполняется операция разблокировки перед последующей операцией блокировки для той же блокировки.
    3. Правило летучих переменных: операция записи в переменную выполняется перед последующей операцией чтения переменной.
    4. Правило доставки: если операция A происходит первой в операции B, а операция B происходит первой в операции C, можно сделать вывод, что операция A происходит первой в операции C.
    5. Правила запуска потока: метод start() объекта Thread выполняется первым для каждого действия этого потока.
    6. Правила прерывания потока: вызов метода прерывания потока () происходит первым, когда код прерванного потока обнаруживает возникновение события прерывания.
    7. Правила завершения потока: все операции в потоке выполняются первыми при обнаружении завершения потока.
    8, правила конца объекта: завершение инициализации объекта сначала происходит в его методе Finalize().

  • Интервьюер: Вы только что упомянули, что JVM изменит порядок наших программ.

  • Я: Нет, это должно соответствовать следующим двум условиям:
    1. Результат выполнения программы нельзя изменить в однопоточной среде.
    2. Изменение порядка не допускается, если есть зависимости данных.
    На самом деле эти два пункта можно свести к одному: JMM допускает произвольный порядок, который не может быть выведен из принципа «происходит до».

  • Я: здесь необходимо упомянутьas-if-serialSEMANTICS: все операции могут быть переупорядочены для оптимизации, но вы должны убедиться, что результат восстановления выполнения не может быть изменен, а компилятор, время выполнения и процессор должны подчиняться как-IF-последовательная семантика. Обратите внимание, что AS-IF-Serial гарантирует только однопоточную среду и недействителен в многопоточной среде. Возьми каштан:

int a=1; //A
int b=2; //B
int c=a+b; //C

Три операции A, B и C имеют следующие отношения: A и B не имеют зависимостей данных, A и C, B и C имеют зависимости данных, поэтому при переупотреблении: A и B можно отсортировать по желанию, но они Должен быть расположен в C. До, но в любом порядке окончательный результат C составляет 3.

  • Далее я сказал: Вот каштан влияния переупорядочения на многопоточность:
public class RecordExample2 {
    int a = 0;
    boolean flag = false;

    /**
     * A线程执行
     */
    public void writer(){
        a = 1;                  // 1
        flag = true;            // 2
    }

    /**
     * B线程执行
     */
    public void read(){
        if(flag){                  // 3
           int i = a + a;          // 4
        }
    }}

Если есть переупорядочение между операцией 1 и операцией 2, это может стать следующим порядком выполнения:
1. Поток A выполняет flag=true;
2. Поток B выполняется if(flag);
3. Поток B выполняет int i = a+a;
4. Поток A выполняет A = 1.
В соответствии с этим порядком выполнения поток B, безусловно, не может прочитать значение a, установленное потоком A, и семантика многопоточности была разрушена из-за переупорядочения здесь. Также возможен переупорядочивание между операцией 3 и операцией 4, которая здесь не описывается. Но между ними существует зависимая от управления связь, потому что операция 4 будет выполнена только в том случае, если будет установлена ​​операция 3. Когда в коде есть управляющая зависимость, это повлияет на параллелизм выполнения последовательности инструкций, поэтому компилятор и процессор будут использовать выполнение с предположением, чтобы преодолеть влияние управляющей зависимости на параллелизм. Если операция 3 и операция 4 переупорядочиваются, а операция 4 выполняется первой, результат вычисления будет временно сохранен в буфере переупорядочивания, а результат вычисления будет записан в переменную i только тогда, когда операция 3 верна.

  • Интервьюер: Можете ли вы сказать мне правду?volatileпонимать?
  • Я: Прежде чем говорить о volatile, давайте добавим в модель памяти Java три концепции: атомарность, видимость и упорядоченность.

1,Видимость: видимость относится к видимости между потоками, состояние, измененное одним потоком, видно другому потоку. То есть результат модификации потока, другой поток может сразу его увидеть. Например: переменные, измененные с помощью volatile, будут видны, переменные, измененные с помощью volatileКэширование внутреннего потока запрещенои переупорядочение, то есть прямое изменение памяти, чтобы она была видна другим потокам. Но обратите внимание на проблему здесь, volatile может только сделать видимым измененный им контент, и не может гарантировать его атомарность. Например volatile int a=0; ++a; эта переменная a имеет видимость, но a++ является неатомарной операцией, что означает, что эта операция также имеет проблемы с безопасностью потоков. В Java volatile/synchronized/final реализует видимость.

2. Атомарность: то есть операция или несколько операций либо выполняются все и процесс выполнения не прерывается никакими факторами, либо ни одна из них не выполняется. **Атомы похожи на транзакции в базе данных, это команда, которая живет и умирает вместе. Взгляните на простой каштан ниже:

i=0;  //1
j=i;  //2
i++; //3
i=j+1; //4

Из четырех вышеперечисленных операций только одна является атомарной операцией, а остальные не являются атомарными. Например, 2 содержит две операции: прочитать i и присвоить значение i переменной j. Атомарность гарантируется в операциях синхронизации/блокировки в Java.

3.Упорядоченный: порядок, в котором выполняется программа, выполняется в том порядке, в котором выполняется код.Переупорядочивание было упомянуто в предыдущем JMM.В модели памяти Java компилятору и процессору разрешено переупорядочивать инструкции для повышения эффективности, и переупорядочивание не повлияет на результат выполнения одного потока, но влияет на многопоточность. Java обеспечивает изменчивость и синхронизацию для обеспечения упорядочения.

  • Я:Принцип volatile заключается в том, что volatile может гарантировать видимость потока и обеспечивать определенный порядок, но не может гарантировать атомарность.В нижнем слое JVM volatile реализуется "барьером памяти". Подвести итог: 1. Гарантированная видимость, а не атомарность. 2. Переупорядочивание инструкций запрещено.
  • Я: Позвольте мне проанализировать эти два свойства volatile.семантика энергозависимой памятиДа: 1. При записи энергозависимой переменной JMM немедленно обновит значение общей переменной в локальной памяти, соответствующей потоку, в основную память.
    2. При чтении энергозависимой переменной JMM делает недействительной локальную память потока и считывает общую переменную непосредственно из основной памяти. Таким образом, семантика записи в память volatile заключается в том, чтобы сбрасывать данные непосредственно в основную память, а семантика чтения в память — считывать непосредственно из основной памяти, что позволяет обеспечить видимость потока.

Итак, семантика энергозависимой памяти заключается в том, как ее достичь? Для общих переменных будет переупорядочено, а для энергозависимых нет, это повлияет на их семантическую память, поэтому для реализации семантической энергозависимой памяти JMM ограничьте переупорядочение.
изменчивые правила переупорядочивания:
1. Если первая операция является непостоянным чтением, независимо от того, что представляет собой вторая операция, ее нельзя переупорядочить. Эта операция гарантирует, что операции, следующие за чтением энергозависимой памяти, не будут переупорядочены компилятором перед чтением энергозависимой памяти.
2. Когда вторая операция является энергозависимой записью, независимо от того, что представляет собой первая операция, ее нельзя переупорядочить. Эта операция гарантирует, что операции перед записью в энергозависимую память не будут переупорядочены компилятором после записи в энергозависимую память.
3. Если первой операцией является запись в энергозависимую память, а второй операцией — чтение в энергозависимую память, их порядок нельзя изменить.

Базовая реализация volatile заключается в вставке барьеров памяти, но компилятору почти невозможно найти оптимальное расположение для минимизации общего числа вставленных барьеров памяти, поэтому JMM использует консервативную стратегию. следующее:
1. Вставьте барьер StoreStore перед каждой операцией энергозависимой записи.
2. Вставьте барьер StoreLoad после каждой операции энергозависимой записи.
3. Вставьте барьер LoadLoad после каждой операции чтения энергозависимой памяти.
4. Вставьте барьер LoadStore после каждой операции чтения энергозависимой памяти.
Сводка: барьер StoreStore-> операция записи-> барьер StoreLoad-> операция чтения-> барьер LoadLoad-> барьер LoadStore. Ниже приведен простой анализ примера: Анализ изменчивого принципа.jpg

  • Интервьюер: Очень хорошо, похоже, вы хорошо разбираетесь в volatile. Давай сменим тему, ты знаешьCASТы можешь поговорить со мной?
  • I: CAS (Сравнить и поменять местами), сравнение и обмен. Синхронизация всей сборки AQS, атомарно-атомарная операция и т. д. основаны на достигнутом CAS, даже в версии ConcurrentHashMap JDK1.8, также настроенной на CAS + синхронизировано. Можно сказать, что CAS является краеугольным камнем всего JUC. Как показано ниже:

  • Я: Внедрение CAS на самом деле не сложно.В CAS есть три параметра: значение памяти V, старое ожидаемое значение A, значение для обновления B, тогда и только тогда, когда значение значения памяти V равно старому ожидаемому значению A, значение значения памяти V будет изменено. на B, иначе ничего не делать, это оптимистическая блокировка.Его псевдокод выглядит следующим образом:
if (this.value == A) {
    this.value = B
    return true;
} else {
    return false;
}
  • Я: Затем я привел пример AtomicInteger, чтобы объяснить интервьюеру реализацию CAS.
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

Выше приведен исходный код AtomicInteger: 1. Unsafe — это основной класс CAS Java не может напрямую обращаться к базовой операционной системе, но через локальные нативные методы. Но, несмотря на это, JVM открыла лазейку: Unsafe, которая обеспечивает Атомарные операции на аппаратном уровне.
2. valueOffset: это адрес смещения значения переменной в памяти.Unsafe получает исходное значение данных через адрес смещения.
3. значение: Текущее значение, измененное с помощью volatile, чтобы убедиться, что одно и то же значение отображается в многопоточной среде.

// AtomicInteger.java
public final int addAndGet(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

// Unsafe.java
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;
}

В методе compareAndSwapInt(var1, var2, var5, var5 + var4) есть четыре параметра, которые представляют: объект, адрес объекта, ожидаемое значение и измененное значение.

  • Я: CAS может гарантировать, что операция чтения-изменения-записи является атомарной операцией, которую легко реализовать на одном процессоре, но немного сложно реализовать на мультипроцессоре. ЦП предоставляет два метода реализации атомарных операций на нескольких процессорах: блокировка шины или блокировка кэша.
    1. Блокировка шины: Блокировка шины заключается в использовании сигнала LOCK#, предоставляемого процессором.Когда процессор выводит этот сигнал на шину, запросы других процессоров будут заблокированы, после чего процессор может использовать только общую оперативную память. Но такой подход, очевидно, Немного властный.
    2. Блокировка кеша: Фактически, для описанной выше ситуации нам нужно только обеспечить, чтобы операция определенного адреса памяти была одновременно атомарной. Блокировка кэша означает, что если данные, кэшированные в области памяти, записываются обратно в память в течение периода блокировки, когда выполняется операция блокировки, Процессор больше не выдает сигнал #LOCK, а изменяет адрес внутренней памяти и использует протокол когерентности кэша для обеспечения атомарности. Механизм когерентности кэша может гарантировать, что данные в одной и той же области памяти могут быть изменены только одним процессором, то есть когда CPU1 изменяет строку кэша. Блокировка кеша используется, когда i находится в CPU2, тогда CPU2 не может одновременно кэшировать строку кеша i.

  • Интервьюер: чтоЧто не так с CAS?

  • Я: Хотя CAS эффективно решает атомную проблему, у него все же есть некоторые недостатки, в основном отраженные в трех аспектах:
    1. Слишком большое время цикла: Если прокрутка CAS будет неудачной в течение длительного времени, это принесет очень большие накладные расходы на ЦП.В JUC в некоторых местах будет ограничено количество прокруток CAS.
    2. Может быть гарантирована атомарная операция только с одной общей переменной: прочитав реализацию CAS, мы знаем, что это можно использовать только для одной общей переменной.Если есть несколько общих переменных, можно использовать только блокировки. Или вы можете использовать CAS для интеграции нескольких переменных в одну переменную.
    3. Проблема ABA: CAS нужно проверить, изменилось ли значение операции, и если нет изменений, то оно будет обновлено, но есть такая ситуация: если значение изначально A, становится B, а затем снова становится A , то в Когда CAS проверит, обнаружит, что изменений нет, но по сути изменилось, что является так называемой проблемой ABA. Решение проблемы ABA состоит в том, чтобы добавить номер версии, то есть добавить номер версии к каждой переменной и прибавлять 1 при каждом изменении, то есть A->B->A, что становится 1A->2B-. >3А. Например, AtomicInteger в атомарном классе будет иметь проблему ABA, а AtomicStampedReference можно использовать для решения проблемы ABA.

Эпилог

Я давно не обновлял серию «Вы брали сегодня интервью?». В интервью по-прежнему очень часто спрашивают многопоточность и параллелизм, но контента JUC слишком много, и в рамках одной статьи его сложно понять. Сегодня первая глава, продолжение следует...

Обратите внимание на публичный аккаунт, чтобы читать замечательные статьи в любое время

Замечательный обзор прошлого

Вы сегодня брали интервью?
редис:nuggets.capable/post/684490…
весна:nuggets.capable/post/684490…
мибатис:nuggets.capable/post/684490…

Серия баз данных
индекс mysql:nuggets.capable/post/684490…
Блокировка базы данных:nuggets.capable/post/684490…
Подтексти библиотеки:nuggets.capable/post/684490…
Транзакции базы данных:nuggets.capable/post/684490…

java спорадическая серия
nuggets.capable/post/684490…
nuggets.capable/post/684490…