Процесс разработки бизнеса на самом деле представляет собой обработку пользовательских бизнес-данных, поэтому основной задачей разработки является обеспечение согласованности и безошибочности данных. В реальных сценариях несколько пользователей будут одновременно читать и записывать одни и те же данные (например, шипы), что приведет к переворачиванию автомобиля без контроля и уменьшит параллелизм, если будет добавлен контроль, что повлияет на производительность и взаимодействие с пользователем.
Как элегантно управлять параллельными данными? По сути нужно решить две проблемы:
- конфликт чтения-записи
- конфликт записи-записи
Давайте посмотрим, как классические параллельные контейнеры Java CopyOnWriteList и ConcurrentHashMap согласовывают эти две проблемы.
CopyOnWriteList
читать и писать стратегию
CopyonWrite как имякопирование при записиСтратегия
Для обработки записи сначала добавьте блокировку ReentrantLock, затем скопируйте копию данных, после внесения изменений в копию замените ссылку на данные данными копии и снимите блокировку после завершения.
Для обработки чтения зависит отvolatileПредоставляет семантические гарантии того, что при каждом чтении будет прочитана последняя ссылка на массив
конфликт чтения-записи
Очевидно, что CopyOnWriteList использует идею разделения чтения и записи для решения конфликта одновременного чтения и записи.
Когда операции чтения и записи происходят одновременно:
- Если операция записи не завершает замену ссылки, операция чтения обрабатывается исходным массивом, и операция обработки копии массива не затрагивается.
- Если операция записи завершила замену ссылки, то операция чтения и операция записи обрабатывают одну и ту же ссылку на массив.
Видно, что при дизайне разделения чтения-записи в процессе одновременного чтения и записи читатель может не иметь возможности видеть последние данные в режиме реального времени, что является так называемой слабой согласованностью.
Именно из-за потери строгой согласованности операции чтения могут выполняться без блокировок и поддерживать большое количество одновременных операций чтения.
конфликт записи-записи
Когда несколько операций записи происходят одновременно, тот, который получает блокировку, выполняется первым, а другие потоки могут блокироваться только до тех пор, пока блокировка не будет снята.
Простой, грубый и эффективный, но производительность параллелизма относительно низкая.
ConcurrentHashMap (JDK7)
читать и писать стратегию
Идея сегментированной блокировки в основном используется для снижения вероятности одновременной работы с частью данных.
Для операций чтения:
- Сначала найдите в массивеSegmentи использоватьUNSAFE.getObjectVolatileСемантика атомарного чтения для получения сегмента
- Впоследствии расположен в массиве с помощью HasthentryUNSAFE.getObjectVolatileСемантика атомарного чтения получает HashEntry
- затем зависеть отfinalНеизменяемый указатель next проходит по связанному списку
- найти соответствующийvolatileценность
Для операций написания:
- Сначала найдите в массивеSegmentи использоватьUNSAFE.getObjectVolatileСемантика атомарного чтения для получения сегмента
- тогда попробуй заблокироватьReentrantLock
- Затем найдите HashEntry в массиве и используйтеUNSAFE.getObjectVolatileСемантика атомарного чтения для получения головного узла списка HashEntry
- Пройдите по связанному списку, если найден существующий ключ, используйтеUNSAFE.putOrderedObjectАтом записывает новое значение, если вы не можете его найти, создайте новый узел, вставьте его в начало цепочки и используйтеUNSAFE.putOrderedObjectЗаголовок списка атомарных обновлений
- Снимите блокировку, когда операция будет завершена
конфликт чтения-записи
Если одновременно читаемые и записываемые данные не находятся в одном и том же сегменте, операции не зависят друг от друга.
Если ConcurrentHashMap расположен в том же сегменте, он использует множество функций Java для разрешения конфликтов чтения и записи, что делает многие операции чтения неблокируемыми.
Когда операция чтения происходит одновременно с операцией записи:
- Если ключ «MED» уже существует, исходное значение напрямую обновляется. В это время операция чтения может прочитать последнее значение под гарантией летучего, без блокировки.
- Если ключ PUT не существует для добавления узла или удаления узла, исходная структура связанного списка будет изменена.Обратите внимание, что каждый следующий указатель HashEntry является окончательным, поэтому связанный список должен быть скопирован, а элементы HashEntry массив (т.е. связанный список) должен быть скопирован.головной узел), обновление завершается с помощью семантической гарантии, предоставляемой UNSAFE.Если операция чтения происходит до обновления нового связанного списка, исходный связанный список все еще получается в это время, блокировка не нужна, но данные не самые свежие
Можно видеть, что поддержка одновременных операций чтения без блокировок все еще слабо согласована.
конфликт записи-записи
Если данные параллельных операций записи не находятся в одном сегменте, операции независимы друг от друга.
Если они расположены в одном и том же сегменте, несколько потоков по-прежнему блокируют ожидание из-за добавления блокировок ReentrantLock.
ConcurrentHashMap (JDK8)
читать и писать стратегию
По сравнению с JDK7 меньше блокировок сегментов, напрямую работающих с массивом узлов (заголовок связанного списка), называемых бакетами.
Для операций чтения передайтеUNSAFE.getObjectVolatileСемантика атомарного чтения для получения последнего значения
Для операций записи из-за метода ленивой загрузки во время инициализации определяется только количество сегментов, а начальное значение по умолчанию отсутствует. Когда требуется значение put, сначала найдите индекс, а затем определите, равно ли значение корзины под индексом null, если да, передайтеUNSAFE.comepareAndSwapObject(CAS), если оно не равно null, добавьте синхронизированную блокировку, найдите значение узла соответствующего связанного списка/красно-черного дерева, измените его, а затем снимите блокировку.
конфликт чтения-записи
Если одновременно читаемые и записываемые данные не находятся в одном сегменте, они независимы друг от друга и не мешают друг другу.
Если в той же бочке, по сравнению с версией JDK7, намного проще, но все же много фич, которые делают неблокирующие операции чтения на основе Java
Когда операция чтения происходит одновременно с операцией записи:
- Если ключ PUT уже существует, значение обновляется напрямую.В это время операция чтения может получить самое последнее значение с гарантией изменчивости
- Если ключ PUT не существует, при создании нового узла или удалении узла исходная структура будет изменена.В это время следующий указатель является изменчивым и вставляется непосредственно в конец связанного списка (это становится красно-черным деревом, когда длина превышает определенную длину) и т. д. Модификации структуры, в это время операция чтения также может получить последнюю следующую
Следовательно, пока операция записи происходит до операции чтения, изменчивая семантика может гарантировать, что данные чтения являются самыми последними.Можно сказать, что ConcurrentHashMap версии JDK8 строго согласован (Здесь мы сосредоточимся только на базовом чтении и записи (GET/PUT), и могут отсутствовать слабо согласованные сценарии, такие как операции расширения емкости, но они должны быть глобально заблокированы.Если есть какие-либо ошибки, укажите на них и учитесь вместе.)
конфликт записи-записи
Если одновременно читаемые и записываемые данные не находятся в одном сегменте, они независимы друг от друга и не мешают друг другу.
Если он находится в одном и том же сегменте, обратите внимание, что операция записи использует разные стратегии в разных сценариях, CAS или Synchronized.
Когда несколько операций записи выполняются одновременно, если ведро равно null, CAS реагирует на одновременные записи.Когда первая операция записи успешно назначена, последующий поток записи CAS завершается сбоем и конкурирует за синхронизированную блокировку, блокировку и ожидание.
резюме
Почему так устроено (личное мнение)
Хранение данных обязательно включает проектирование структуры данных, и любая операция с данными должна основываться на структуре данных.
Общая идея состоит в том, чтобы заблокировать всю структуру данных, но существование блокировки значительно повлияет на производительность, поэтому следующая задача - найти, какие операции могут быть разблокированы
Существует два основных типа операций: чтение и запись.
Давайте сначала посмотрим на запись. Поскольку она включает в себя изменения исходных данных, она обязательно перевернется без контроля. Как это контролировать?
Есть также два типа операций записи: один изменяет структуру, а другой нет.
Для записей, которые изменят структуру, независимо от того, является ли базовый слой массивом или связанным списком, поскольку изменения основаны на исходной структуре, она должна быть сериализована для обеспечения атомарных операций Точка оптимизации — это оптимизация блокировки Блокировка на блокировку ReentrantLock ConcurrentHashMap версии 1.7, а затем на блокировку Synchronized Improved версии 1.8. Или децентрализация данных, структуры данных на основе хэшей, такие как concurrnethashmap, имеют больше преимуществ децентрализации сегментов, чем структуры данных CopyOnWriteList.
Для записей, которые не изменяют структуру или частота изменений невелика (частота расширения корзины низкая), поскольку накладные расходы на блокировку слишком велики, CAS — хорошая идея. Почему CopyOnWriteList не использует CAS для управления одновременной записью, я лично думаю, что основная причина в том, что структура часто меняется.Вы можете посмотреть на контейнеры массивов на основе CAS, такие как ActomicReferenceArray, которые не позволяют структуре изменяться после их создания.
Убедившись, что данные не исправлены, их относительно легко прочитать.
Основное внимание состоит в том, следует ли читать последние данные в режиме реального времени (в ожидании завершения операции записи), то есть проблема сильной или слабой последовательности
Если вы сильны, у вас будет одинаковый способ чтения, чтения и записи одной и той же блокировки, что влияет на эффективность чтения и записи.
В большинстве сценариев требования к согласованности данных для чтения не такие высокие, как для записи: они могут быть прочитаны неправильно, но не должны быть неправильно записаны. Если в момент чтения данные не были изменены, не имеет значения, читаются ли старые данные, пока последняя запись завершена, они видны читающим.
К счастью, JMM (модель памяти Java) имеет семантику непостоянной видимости, которая может гарантировать, что чтение и запись измененных данных можно увидеть без блокировки. Кроме того, существуют различные прямые операции пакета UNSAFE с памятью, которые также могут выполнять семантику видимости с относительно высокой производительностью.
Для операций чтения лучшими данными являются неизмененные данные, поэтому вам не нужно беспокоиться о различных проблемах, вызванных модификацией. Единственная константа - это изменение, и некоторые данные все еще могут изменяться. Если вы хотите поддерживать такую инвариантность или попытаться уменьшить частоту изменений, измененные части должны обрабатываться в другом месте, что является так называемым чтением-записью. разделение.
Вышеупомянутое является чисто личным пониманием, ограниченным уровнем, идея может быть неверной, добро пожаловать на обсуждение и предоставление указателей.
Рекомендуемое чтение
Принцип реализации CONCULLENTASHMAP и интерпретация исходного кода
ConcurrentHashMap для параллельных контейнеров (версия JDK 1.8)