предисловие
Эта статья изConcurrentHashMap
Общие вопросы на собеседовании знакомят с темами и постепенно раскрывают принципы их построения.Я считаю, что после прочтения этой статьи будет большим подспорьем сопутствующие вопросы на собеседовании.
HashMap
Один из наиболее часто используемых классов инструментов в нашей повседневной разработке, но использованиеHashMap
Одна из самых больших проблем заключается в том, что это небезопасно для потоков. Что, если нам нужна безопасность потоков? Затем вы можете использоватьConcurrentHashMap
,ConcurrentHashMap
а такжеHashMap
Функция в основном та же,ConcurrentHashMap
даHashMap
Поточно-безопасная версия .
потому чтоConcurrentHashMap
а такжеHashMap
существуетjdk1.8
С точки зрения защиты резьбы, другие проекты очень похожи, поэтому есть много одинаковых дизайнерских идей.Эта статья не будет повторяться слишком много, если вы не понимаетеHashMap
Основной принцип реализации, рекомендуется сначала прочитать эту статьюGold Three Silver Four Help интервью - легко читаемый исходный код HashMapК пониманиюHashMap
дизайнерское мышление.
Принцип ConcurrentHashMap
ConcurrentHashMap
даHashMap
Поточно-безопасная версия , которая внутренне иHashMap
Точно так же это реализовано и в виде массива + связанного списка + красно-черного дерева.
Как добиться потокобезопасности? замок. Но как добавить этот замок? существуетHashTable
, находится непосредственно вput
а такжеget
добавлено в методsynchronized
, теоретическиConcurrentHashMap
Вы также можете сделать это, но степень детализации блокировки слишком велика, что сильно повлияет на производительность параллелизма.ConcurrentHashMap
Он не использует такой прямой, простой и грубый метод, он использует очень тонкий внутренний дизайн, который значительно снижает конкуренцию блокировок и улучшает производительность параллелизма.
ConcurrentHashMap
инициализация в иHashMap
То же, что и в , и емкость также будет приведена к N степени числа 2, и причина этого здесь повторяться не будет.
Какие улучшения были внесены в ConcurrentHashMap в JDK1.8
существуетJDK1.7
версия,ConcurrentHashMap
Он реализован блокировкой массива + сегмента + сегмента, а его внутренняя часть разделена на сегменты (Segment
) множество,Segment
по наследствуReentrantLock
запирать, запирая по одномуsegment
для уменьшения детализации блокировки и обеспечения того, чтобы каждыйsegment
Потокобезопасность операций внутри него, что обеспечивает глобальную поточно-безопасность. На картинке нижеJDK1.7
в версииConcurrentHashMap
Принципиальная схема конструкции:
Но недостатком этого является то, что каждый проходhash
Требуется подтвердить местоположение2
раз, чтобы найти текущийkey
В какой слот он должен попасть:
- пройти через
hash
ценность и段数组长度-1
Выполните битовую операцию для подтверждения текущегоkey
к какому сегменту он принадлежит, то есть подтвердить, что он находится вsegments
Положение массива. - пройти снова
hash
ценность иtable
массив (т.е.ConcurrentHashMap
Массив базовых данных хранилища) length — 1 Выполнение побитовых операций для подтверждения своего сегмента.
Для дальнейшей оптимизации производительности вjdk1.8
версия, даConcurrentHashMap
Оптимизирован, отменен дизайн сегментного замка и заменен наcas
операция иsynchronized
ключевые слова для достижения оптимизации, а идея «разделяй и властвуй» также используется для повышения эффективности расширения во время расширения.JDK1.8
серединаConcurrentHashMap
структура хранения иHashMap
В основном то же самое, как показано на следующем рисунке:
Почему ключ и значение не могут быть нулевыми
существуетHashMap
середина,key
а такжеvalue
все может бытьnull
, но вConcurrentHashMap
В Китае это запрещено, почему?
авторDoug Lea
Я сам ответил на этот вопрос в параллельном программировании,null
Значение легко привести к двусмысленности, если первый вызовget(key)
Возвращаемый результатnull
тогда мы не можем быть уверены, потому что в то время этоkey
соответствующийvalue
положить его в себяnull
, или сказать этоkey
Значение вообще не существует, что могло бы вызвать двусмысленность, и если в непараллельном программировании можно было бы пойти дальше, вызвавcontainsKey
метод для оценки, но в параллельном программировании нет никакой гарантии, что между двумя методами нет других потоков для измененияkey
значение, так что это прямо запрещеноnull
существование ценности.
и авторDoug Lea
сама также считает, что если разрешено в таких наборах, какmap
а такжеset
ждать, чтобы существоватьnull
значение, даже в непараллельных коллекциях, имеет смысл публично разрешать ошибки в программе, что такжеDoug Lea
а такжеJosh Bloch
(HashMap
один из авторов) был одним из немногих особых мнений по вопросам дизайна, в то время какConcurrentHashMap
даDoug Lea
Он был разработан одним человеком, поэтому был забанен напрямуюnull
существование ценности.
Как ConcurrentHashMap гарантирует безопасность потоков
существуетConcurrentHashMap
В , большое количество идей «разделяй и властвуй» используется для уменьшения детализации блокировок и повышения производительности параллелизма. Его исходный код использует многоcas
операции по обеспечению безопасности, а неHashTable
То же, каким бы методом ни пользоваться, прямо и грубоsynchronized
ключевые слова для достижения, следующий анализ принципа, часть иHashMap
В этой статье сходства не повторяются, в этой статье в основном анализируются аспекты безопасности.ConcurrentHashMap
дизайн.
Как использовать CAS для обеспечения безопасности инициализации массива
Вот как инициализировать:
Есть очень важная переменнаяsizeCtl
, эта переменная важна для понимания всегоConcurrentHashMap
Принцип очень важен.
sizeCtl
Имеет четыре значения:
-
sizeCtl<-1
указывает на то, что естьN-1
потоки выполняют операции масштабирования, такие как-2
значит есть2-1
нити расширяются. -
sizeCtl=-1
Заполнитель, указывающий, что массив в данный момент инициализируется. -
sizeCtl=0
Состояние по умолчанию, указывающее, что массив не был инициализирован. -
sizeCtl>0
Запишите размер, который необходимо увеличить в следующий раз.
Зная значение этой переменной, описанный выше метод легко понять, и вторая ветвь принимаетCAS
операция, потому чтоSIZECTL
По умолчанию0
, поэтому, если замена может быть успешной, текущий поток может выполнить операцию инициализации,CAS
Не удалось, что указывает на то, что другие потоки взяли на себя инициативуsizeCtl
изменился на-1
. После успешного расширения порог для следующего расширения будет назначен наsc
,Прямо сейчасsizeClt
.
Как операция put гарантирует видимость элементов массива
ConcurrentHashMap
данные, хранящиеся вNode
используется массивvolatile
Чтобы изменить, но это может только гарантировать, что ссылка на массив доступна между разными потоками, это не гарантирует, что элементы внутри массива также видны между потоками, поэтому здесь мы определяем, есть ли в индексе элемент, и Он не может доступ напрямую через индексы, так как же к нему обращаться? Исходный код дает вам ответ:
Как видите, здесь черезtabAt
метод для получения элемента, в то время какtableAt
метод на самом деле являетсяCAS
работать:
Если текущий элемент узла оказывается пустым, он также передается черезCAS
работать(casTabAt
) для сохранения текущего элемента.
Если текущий элемент узла не пуст, он будет использоватьсяsynchronized
Ключевое слово блокирует текущий узел и выполняет соответствующую операцию настройки:
тонкий способ подсчета
существуетHashMap
, вызовput
затем метод пройдет++size
способ хранения количества элементов в текущей коллекции, но в параллельном режиме эта операция не безопасна, поэтому ее нельзя сделать таким способом, то можно ли это сделать с помощьюCAS
операция по изменениюsize
Шерстяная ткань?
непосредственно черезCAS
операция по изменениюsize
Это осуществимо, но если есть много потоков для изменения одновременноsize
операция, то только один поток может успешно заменить ее, а другие потоки могут только продолжать попыткиCAS
, что влияетConcurrentHashMap
Спектакль поставлен, поэтому автор придумал идею «разделяй и властвуй», чтобы завершить счет.
Автор определяет массив для подсчета, и массив, используемый для подсчета, также может быть расширен.Каждый раз, когда потоку необходимо подсчитать, он получает позицию индекса массива случайным образом для работы, так что блокировка может быть уменьшена как насколько это возможно.size
Когда счет достигается путем обхода массива:
//用来计数的数组,大小为2的N次幂,默认为2
private transient volatile CounterCell[] counterCells;
@sun.misc.Contended static final class CounterCell {//数组中的对象
volatile long value;//存储元素个数
CounterCell(long x) { value = x; }
}
метод подсчета addCount
Далее посмотримaddCount
метод:
первый судьяCounterCell
Является ли массив пустым, что должно быть здесь, здесьCAS
операция заключается вBASECOUNT
а такжеbaseCount
Сравните, если они равны, это означает, что нет других потоков для измененияbaseCount
(которыйCAS
операция выполнена успешно), вам не нужно использоватьCounterCell
массив, при непосредственном использованииbaseCount
считать.
еслиCounterCell
Быть пустым иCAS
Не удается, это будет звонитьfullAddCount
путь кCounterCell
Массив инициализирован.
метод fullAddCount
Этот метод тоже очень длинный и выглядит более сложным, в котором содержится правильноеCounterCell
Инициализация массива и операции присваивания.
Инициализировать массив CounterCell
Проигнорируем его и перейдем непосредственно к логике инициализации:
Есть еще одна важная переменнаяcellsBusy
, по умолчанию0
, указывающее, что ни один поток в настоящее время не инициализируется или не расширяется, поэтому здесь оценивается, еслиcellsBusy==0
,а такжеas
На самом деле впереди стоит поместить глобальную переменнуюCounterCell
Назначение массива, причина повторной оценки здесь - подтвердить, изменили ли другие потоки глобальный массив.CounterCell
, так что если условие выполнено, оно пройдетCAS
Модификация операцииcellsBusy
для1
, указывая на то, что в данный момент он инициализирует себя, и другие потоки не могут одновременно выполнять операцию инициализации.
Наконец, вы можете видеть, что по умолчанию используется длина2
Массив , то есть использование2
Расположение места для храненияConcurrentHashMap
Количество элементов.
Как назначается CounterCell
После завершения инициализации, если вы позвоните сноваput
метод, то он войдетfullAddCount
Другая ветвь метода:
Вот первое решениеCounterCell
Массив не пустой, то снова будем определять, что элементы в массиве не пусты, потому что, если элемент пуст, вам нужно инициализироватьCounterCell
объект в массив, и если элемент не пустой, вам нужно толькоCAS
Операция заменяет количество в элементе.
Так что логика тут тоже очень понятная, инициализацияCounterCell
объект также должен бытьcellBusy
Зависит от0
изменить на1
.
Может ли массив счетчиков CounterCell также расширяться?
Наконец, продолжаем смотреть на другие ветки:
В основном смотрите на ветку в красном поле на рисунке выше.Раз вы заходите в эту ветку, значит все предыдущие ветки не выполняются, а именно:
- ток
CounterCell
Массив инициализирован. - В настоящее время прошло
hash
рассчитанныйCounterCell
Элемент в индексе массива неnull
. - непосредственно через
CAS
Модификация операцииCounterCell
Число объектов в указанной позиции индекса в массиве не удалось, что указывает на то, что другие потоки конкурируют за изменение элементов в одном и том же индексе массива. - Текущая операция не соответствует условиям, запрещающим расширение.
- Никакие другие темы в настоящее время не создают новые
CounterCell
Массивы и текущийCounterCell
Размер массива по-прежнему меньше, чемCPU
количество.
Итак, далее вам нужноCounterCell
Массив также расширяется.Метод расширения иConcurrentHashMap
То же, что расширение2
, так на самом делеCounterCell
Емкость массива также должна удовлетворять степени N числа 2.
Расширение ConcurrentHashMap
Далее нам нужно вернуться кaddCount
метод, потому что этот метод также определяет текущее количество элементов при добавлении количества элементов.ConcurrentHashMap
Достиг ли размер порога расширения, и если да, то его необходимо расширить.
Может ли расширение также поддерживать параллелизм?
Что может вас здесь удивить, так это то,ConcurrentHashMap
Расширение также поддерживает многопоточность одновременно, как это делается? Вернемся кaddCount
способ узнать.
здесьcheck
длина передаваемого связанного списка,>=0
Только начал проверять, нужно ли расширение, и сразу послеwhile
Цикл в основном удовлетворяет двум условиям:
- Ранее мы упоминали,
sizeCtl
При инициализации ему будет присвоен размер следующего расширения (тоже после расширения), поэтому>=sizeCtl
Указывает, достигнут ли порог расширения. -
table
не дляnull
И текущая длина массива меньше 30-й степени максимального значения 2.
Какая польза от штампа расширения
Когда условия расширения выполняются, сначала вызывается метод для получения штампа расширения. Этот штамп расширения интересен. Чтобы понять штамп расширения, его необходимо проанализировать с двоичной точки зрения.resizeStamp
Метод в одном предложении, в которомRESIZE_STAMP_BITS
Является значением по умолчанию16
.
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
Ключ здесьInteger.numberOfLeadingZeros(n)
Этот метод, исходный код этого метода не будет опубликован, на самом деле этот метод должен делать одну вещь, то естьПолучить наибольшее незначение после преобразования текущих данных в двоичные0
передний0
номер.
Это предложение немного неуклюже. Давайте возьмем пример.16
превалирует,16
Преобразованный в двоичный файл10000
, высший не-0
бит находится в5
немного, потому чтоint
тип32
положение, поэтому он до сих пор27
немного, и то и другое0
, то результатом этого метода является27
(1
спереди27
индивидуальный0
).
потом1 << (RESIZE_STAMP_BITS - 1)
В текущей версии это1<<15
, то есть получить двоичное число1000000000000000
, вот тоже одно дело, поставить это1
перейти к16
кусочек. Последние два числа проходят|
Результат операции должен быть первым16
бит1
,потому чтоint
да32
биты, максимум32
индивидуальный0
, и потому чтоn
Размер по умолчанию16
(ConcurrentHashMap
размер по умолчанию), так что на практике самое большее27
(11011
), то есть старшая цифра числа1
Также только на пятом месте, выступая|
Большинство операций означает низкое воздействие5
битый результат.
27
преобразовать в двоичный0000000000000000000000000011011
, затем с00000000000000001000000000000000
воплощать в жизнь|
операции, конечный результат00000000000000010000000000011011
,
Примечание. Причина гарантии16
бит1
, чтобы убедиться, чтоsizeCtl
Переменная является отрицательным числом, потому что, как мы упоминали ранее, отрицательное число этой переменной означает, что в данный момент расширяются потоки.sizeCtl
Отношения будут описаны позже.
Почему первое расширение считает +2 вместо +1
Первое расширение будет следовать не первым двум условиям, а последнему условию красного прямоугольника, которое проходитCAS
операция будетrs
сдвинулся влево16
(Resize_stamp_shift), затем плюс один2
Что это значит? Почему он добавлен?2
Шерстяная ткань?
Чтобы ответить на этот вопрос, давайте сначала ответим на другой вопрос, штамп расширения, полученный вышеописанным способом.rs
Какая польза? На самом деле этот штамп расширения представляет собой два значения:
- высоко
16
Бит представляет текущую метку расширения, которую можно понимать как эпоху. - Низкий
16
Биты представляют количество потоков для масштабирования.
Знание этих двух условий легко понять, потому чтоrs
в конечном итоге назначаетсяsizeCtl
, покаsizeCtl
Отрицательные числа представляют собой расширение, в то время какrs
сдвиг влево16
бит просто так, что старший бит1
, в это время низкий16
биты все0
, а поскольку низкая16
бит для записи количества потоков расширения, так что должно быть+1
, но вот+2
, Причина вsizeCtl
середина-1
Это значение было использовано для замены текущего потока, готового к расширению, поэтому, если вы напрямую+1
Это будет конфликтовать с битом флага.
Итак, продолжайте возвращаться ко второму красному квадрату на картинке выше, то есть продолжайте в обычном режиме.+1
Теперь это требуется только при инициализации первой записи количества потоков расширения.+2
.
Условия расширения
Далее продолжаем смотреть на первое красное поле на картинке выше, которое содержит5
состояние, а это означает, что5
Ни одно из условий не будет расширено:
-
(sc >>> RESIZE_STAMP_SHIFT) != rs
Это состояние на самом деле имеетbug
,существуетJDK12
был заменен. -
sc == rs + 1
Указывает, что последний поток расширения выполняет первую работу, а также означает, что расширение подходит к концу. -
sc == rs + MAX_RESIZERS
Указывает, что максимальное количество расширенных потоков было достигнуто, поэтому потоки не могут продолжать добавлять к расширению. - После того, как расширение будет завершено,
nextTable
(новый массив для расширения) установлен вnull
. -
transferIndex <= 0
Указывает, что все индексы, доступные в настоящее время для расширения, выделены, что также представляет собой конец расширения текущего потока.
Как реализовать расширение в условиях множественного параллелизма
Как добиться расширения при множественном параллелизме без конфликтов? Возможно, все думали о принятии идеи «разделяй и властвуй».ConcurrentHashMap
Используется метод сегментированного расширения, то есть каждый поток отвечает за один сегмент, а минимум по умолчанию равен16
, то есть еслиConcurrentHashMap
только в16
slot, то в расширении будет участвовать только один поток. если больше, чем16
то по текущемуCPU
Максимальное количество потоков, участвующих в расширении, не будет превышатьCPU
количество.
простор для расширения иHashMap
Точно так же каждое расширение должно сдвигать исходный размер пространства на один бит влево, то есть увеличивать его вдвое по сравнению с предыдущим размером. Обратите внимание здесьtransferIndex
Представляет расширенный нижний индекс, который по умолчанию соответствует размеру старого массива.
Как обеспечить безопасность переноса данных при расширении
После инициализации нового массива следующим шагом будет подготовка к подтверждению границ. То есть необходимо подтвердить слот, за который отвечает текущий поток, после подтверждения он будет двигаться вперед от большого к меньшему, например, первый поток отвечает за1-16
, то соответствующая граница массива0-15
, то начнется с последней цифры15
Начните перенос данных:
И сравнивались три ключевые переменные:
-
fwd
Узел, это представляет собой узел-заполнитель, наиболее важным является узелhash
значение-1
, так что когда-то найденный в узлеhash
значение-1
Вы можете знать, что текущий узел был перенесен. -
advance
: указывает, можно ли переместить следующий слот.Его можно установить на текущий слот только после переноса данных.true
-
finishing
: завершена ли миграция данных.
Зная эти переменные, посмотрите еще раз на приведенный выше код, он обязательно войдет с первого разаwhile
цикл, потому что по умолчаниюadvance
дляtrue
, целью первого входа в цикл является подтверждение границы, так как граничное значение не было подтверждено, он пойдет сразу на последнюю ветвь, черезCAS
Действие Подтвердить границу.
Здесь трудно понять прямое выражение границы подтверждения, проиллюстрируем его на примере:
Предположим, что начальное пространство16
, то расширенное пространство равно32
,В настоящее времяtransferIndex
это старый размер массива16
, а во второмif
судить,transferIndex
назначен наnextIndex
,такnextIndex
для1
,а такжеstride
Представляет количество слотов, за которые отвечает каждый поток, минимум16
,такstride
Слишком16
,такnextBound= nextIndex > stride ? nextIndex - stride : 0
Все могут получить:nextBound=0
а такжеi=15
Это также текущий поток0-15
индексы массива , и от0
Начать продвижение, сразу после подтверждения границыadvance
Установить какfalse
, то есть выскочитwhile
цикл для выполнения следующей части логики переноса данных.
ПС: потому чтоnextBound=0
,такCAS
Операция на самом делеtransferIndex
стал0
, что указывает на то, что все индексы текущего расширенного массива были выделены, что также является первой проблемой, которая не удовлетворяет расширению.5
состояние.
Во время переноса данных используйтеsynchronized
Ключевое слово блокирует текущий узел, что означает, что степень детализации блокировки является точной для каждого узла, что, можно сказать, значительно повышает эффективность. Миграция данных после блокировки иHashMap
В основном то же самое, миграция также завершается различением высоких и низких позиций, которые не будут повторяться в этой статье.
После того, как текущий узел завершит миграцию данных,advance
переменная будет установлена вtrue
, то есть вы можете продолжать продвигать узел, так что он снова войдет в указанное вышеwhile
Первые две ветви цикла, нижний индексi
двигаться вперед сноваadvance
Установить какfalse
, затем повторяйте до тех пор, пока нижний индекс не перейдет к0
Полная миграция данных.
while
После того, как цикл полностью завершится, он войдет в следующееif
Судя по красному квадратику, после того, как текущий поток завершит сам миграцию, количество потоков расширения будет уменьшено.После уменьшения снова будет передано условие.Это условие фактически является обратным предыдущему условию перед входом в расширения. Если он установлен, расширение было завершено. , после завершения расширения,nextTable
Установить какnull
, поэтому приведенное выше не удовлетворяет разложению4
Здесь ставится условие.
Суммировать
В этой статье в основном описываетсяConcurrentHashMap
Как обеспечить безопасность, и выбрали некоторые из наиболее распространенных классических вопросов для интервью, чтобы проанализировать и ответить на них в целом.ConcurrentHashMap
В , вся идея состоит в том, чтобы уменьшить степень детализации блокировок и уменьшить конкуренцию блокировок, поэтому принимается множество идей «разделяй и властвуй», таких как одновременное многопоточное расширение и с помощью массива для достиженияsize
считать и др.