Java ConcurrentMap - Java ConcurrentMap

Язык программирования Java Платформа коллекций Java версия 1.5 и выше определяет и реализует исходные обычные однопоточные карты, а также новые потокобезопасные карты, реализующие java.util.concurrent.ConcurrentMapинтерфейс среди других параллельных интерфейсов. В Java 1.6 java.util.NavigableMap добавлен интерфейс, расширяется java.util.SortedMap, а java.util.concurrent.ConcurrentNavigableMap интерфейс был добавлен как комбинация подинтерфейсов.

Интерфейсы Java Map

Схема интерфейса карты версии 1.8 имеет вид ниже. Наборы можно рассматривать как подварианты соответствующих карт, в которых значения всегда являются определенной константой, которую можно игнорировать, хотя API набора использует соответствующие методы с разными именами. Внизу находится java.util.concurrent.ConcurrentNavigableMap, который является множественным наследованием.

Реализации

ConcurrentHashMap

Для неупорядоченного доступа, как определено в интерфейсе java.util.Map, java.util.concurrent.ConcurrentHashMap реализует java.util.concurrent.ConcurrentMap. Механизм представляет собой хеш-доступ к хеш-таблице со списками записей, каждая из которых содержит ключ, значение, хэш и следующую ссылку. До Java 8 было несколько блокировок, каждый из которых сериализовал доступ к «сегменту» таблицы. В Java 8 встроенная синхронизация используется в заголовках самих списков, и списки могут видоизменяться в небольшие деревья, когда они угрожают стать слишком большими из-за неудачных коллизий хешей. Кроме того, Java 8 оптимистично использует примитив сравнения и установки для размещения начальных заголовков в таблице, что очень быстро. Производительность O (n), но иногда случаются задержки, когда требуется повторное хеширование. После расширения хеш-таблица никогда не сжимается, что может привести к «утечке» памяти после удаления записей.

ConcurrentSkipListMap

Для упорядоченного доступа, как определено интерфейсом java.util.NavigableMap, java.util.concurrent.ConcurrentSkipListMap был добавлен в Java 1.6 и реализует java.util.concurrent.ConcurrentMap, а также java.util.concurrent.ConcurrentNavigableMap. Это Пропустить список который использует методы Lock-free для создания дерева. Производительность - O (log (n)).

Ctrie

  • Ctrie Дерево Lock-free на основе дерева.

Проблема одновременной модификации

Одной из проблем, решаемых пакетом Java 1.5 java.util.concurrent, является проблема одновременной модификации. Предоставляемые им классы коллекций могут надежно использоваться несколькими потоками.

Для всех совместно используемых потоков непараллельных карт и других коллекций необходимо использовать какую-либо форму явной блокировки, например собственную синхронизацию, чтобы предотвратить одновременную модификацию, или же должен быть способ доказать с помощью логики программы, что одновременная модификация невозможна. Одновременная модификация карты несколькими потоками иногда нарушает внутреннюю согласованность структур данных внутри карты, что приводит к ошибкам, которые проявляются редко или непредсказуемо и которые трудно обнаружить и исправить. Кроме того, одновременная модификация одним потоком с доступом для чтения другим потоком или потоками иногда дает непредсказуемые результаты для читателя, хотя внутренняя согласованность карты не будет нарушена. Использование внешней программной логики для предотвращения одновременной модификации увеличивает сложность кода и создает непредсказуемый риск ошибок в существующем и будущем коде, хотя позволяет использовать непараллельные Коллекции. Однако ни блокировки, ни логика программы не могут координировать внешние потоки, которые могут контактировать с Коллекцией.

Счетчики модификаций

Чтобы помочь с проблемой одновременной модификации, не параллельные реализации Map и другие Коллекции используют внутренние счетчики модификации, с которыми консультируются до и после чтения, чтобы следить за изменениями: писатели увеличивают счетчики модификации. Предполагается, что параллельная модификация обнаруживается этим механизмом, вызывая исключение java.util.ConcurrentModificationException, но это не гарантируется во всех случаях, и на него не следует полагаться. Счетчик обслуживания также снижает производительность. Из соображений производительности счетчики не изменяются, поэтому не гарантируется, что их изменения будут распространяться между потоками.

Collections.synchronizedMap ()

Одним из решений проблемы одновременной модификации является использование определенного класса-оболочки, предоставляемого фабрикой в ​​Коллекциях: public static Map synchronizedMap (Map m) который обертывает существующую не поточно-ориентированную карту с методами, которые синхронизируются на внутреннем мьютексе. Есть также обертки для других видов Коллекций. Это частичное решение, потому что все еще возможно, что базовая карта может быть непреднамеренно доступна для потоков, которые сохраняют или получают развернутые ссылки. Кроме того, все Коллекции реализуют java.lang.Iterable но синхронизированные обернутые Карты и другие обернутые Коллекции не предоставляют синхронизированные итераторы, поэтому синхронизация предоставляется клиентскому коду, который является медленным и подверженным ошибкам, и невозможно ожидать дублирования другими потребителями синхронизированной Карты. Все время итерации также должно быть защищено. Кроме того, карта, которая дважды обернута в разных местах, будет иметь разные внутренние объекты-мьютекс, с которыми работает синхронизация, что позволяет перекрывать друг друга. Делегирование снижает производительность, но современные компиляторы Just-in-Time часто сильно встраиваются, что ограничивает снижение производительности. Вот как работает упаковка внутри оболочки - мьютекс - это всего лишь последний объект, а m - последняя завернутая карта:

    общественный V положить(K ключ, V ценить) {        синхронизированный (мьютекс) {возвращаться м.положить(ключ, ценить);}    }

Синхронизация итерации рекомендуется следующим образом, однако она синхронизируется с оболочкой, а не с внутренним мьютексом, что позволяет перекрывать друг друга:[1]

    карта<Нить, Нить> wrappedMap = Коллекции.synchronizedMap(карта);          ...    синхронизированный (wrappedMap) {        за (Нить s : wrappedMap.keySet()) {            // возможно выполнение какой-то долгой операции возможно             // много раз, задерживая все остальные обращения        }    }

Родная синхронизация

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

    карта<Нить, Нить> карта = новый HashMap<Нить, Нить>();    ...    // Поток A    // Используем саму карту как замок. Вместо этого можно использовать любой согласованный объект.    синхронизированный(карта) {       карта.положить("ключ","ценить");    }    ..    // Поток B    синхронизированный (карта) {        Нить результат = карта.получать("ключ");         ...     }    ...    // Поток C    синхронизированный (карта) {        за (Вход<Нить, Нить> s : карта.entrySet()) {            /*             * Некоторые из них могут работать медленно, задерживая все другие предположительно быстрые операции.              * Синхронизация на отдельных итерациях невозможна.             */             ...        }    }

ReentrantReadWriteLock

Код, использующий java.util.concurrent.ReentrantReadWriteLock, похож на код для собственной синхронизации. Однако в целях безопасности блокировки должны использоваться в блоке try / finally, чтобы ранний выход, такой как выброс исключения или break / continue, обязательно прошел через разблокировку. Этот метод лучше, чем использование синхронизации, потому что чтения могут перекрывать друг друга, возникает новая проблема при принятии решения о том, как приоритизировать записи по отношению к чтениям. Для простоты вместо него можно использовать java.util.concurrent.ReentrantLock, который не делает различий между чтением и записью. Возможно больше операций с замками, чем с синхронизацией, например tryLock () и tryLock (длинный тайм-аут, единица TimeUnit).

    окончательный ReentrantReadWriteLock замок = новый ReentrantReadWriteLock();    окончательный ReadLock readLock = замок.readLock();    окончательный WriteLock writeLock = замок.writeLock();    ..    // Поток A    пытаться {        writeLock.замок();        карта.положить("ключ","ценить");        ...    } наконец-то {        writeLock.разблокировать();    }    ...    // Поток B    пытаться {        readLock.замок();        Нить s = карта.получать("ключ");        ..    } наконец-то {        readLock.разблокировать();    }     // Поток C    пытаться {        readLock.замок();        за (Вход<Нить, Нить> s : карта.entrySet()) {            /*             * Некоторые из них могут работать медленно, задерживая все другие предположительно быстрые операции.              * Синхронизация на отдельных итерациях невозможна.             */             ...        }    } наконец-то {        readLock.разблокировать();    }

Конвои

Взаимное исключение имеет Блокировать конвой проблема, при которой потоки могут накапливаться на блокировке, в результате чего JVM необходимо поддерживать дорогостоящие очереди официантов и «парковать» ожидающие потоки. Парковать и отменять парковку потока стоит дорого, и может произойти медленное переключение контекста. Для переключения контекста требуется от микросекунд до миллисекунд, в то время как собственные базовые операции карты обычно занимают наносекунды. По мере роста конкуренции производительность может упасть до небольшой части пропускной способности одного потока. Однако, когда конкуренция за блокировку отсутствует или незначительна, это мало влияет на производительность, за исключением теста конкуренции блокировки. Современные JVM встраивают большую часть кода блокировки, сокращая его до нескольких инструкций, что очень быстро позволяет избежать конфликтов. Однако методы повторного входа, такие как собственная синхронизация или java.util.concurrent.ReentrantReadWriteLock, имеют дополнительный багаж, снижающий производительность при поддержании глубины повторного входа, что также влияет на случай отсутствия конкуренции. Проблема Convoy, похоже, решается с помощью современных JVMS, но ее можно скрыть медленным переключением контекста: в этом случае задержка увеличится, но пропускная способность останется приемлемой. С сотнями потоков время переключения контекста 10 мс дает задержку в секундах.

Несколько ядер

Решения по взаимному исключению не могут использовать все вычислительные мощности многоядерной системы, потому что в коде карты одновременно разрешается только один поток. Реализации конкретных параллельных карт, предоставляемых Java Collections Framework и другими, иногда используют преимущества нескольких ядер, используя Заблокировать бесплатно методы программирования. В методах блокировки используются такие операции, как встроенный метод compareAndSet (), доступный во многих классах Java, таких как AtomicReference, для выполнения условных обновлений некоторых внутренних структур карты атомарно. Примитив compareAndSet () дополнен в классах JCF собственным кодом, который может выполнять compareAndSet на специальных внутренних частях некоторых объектов для некоторых алгоритмов (используя «небезопасный» доступ). Методы сложны, часто полагаются на правила межпоточного взаимодействия, обеспечиваемые изменчивыми переменными, отношением «происходит до», специальными видами «циклов повтора» без блокировок (которые не похожи на спиновые блокировки тем, что они всегда производят прогресс) . CompareAndSet () полагается на специальные инструкции для процессора. Любой код Java может использовать для других целей метод compareAndSet () в различных параллельных классах, чтобы добиться параллелизма без блокировки или даже без ожидания, что обеспечивает конечную задержку. Техники блокировки просты во многих распространенных случаях и с некоторыми простыми коллекциями, такими как стеки.

На диаграмме показано, как синхронизация с использованием Collections.synchronizedMap (), обертывающего обычную HashMap (фиолетовый), может не масштабироваться так же хорошо, как ConcurrentHashMap (красный). Остальные - это упорядоченные ConcurrentNavigableMaps AirConcurrentMap (синий) и ConcurrentSkipListMap (зеленый CSLM). (Плоские места могут быть повторными хэшами, создавая таблицы, которые больше, чем Nursery, а ConcurrentHashMap занимает больше места. Обратите внимание, что на оси Y должно быть указано «ставит K». Система - 8-ядерный i7 2,5 ГГц, с -Xms5000m для предотвращения сборки мусора). Расширение процессов GC и JVM значительно меняет кривые, а некоторые внутренние методы Lock-Free генерируют мусор при конкуренции.

Обе хеш-таблицы быстрые

Масштабируются только упорядоченные карты, а синхронизированная карта откатываетсяСинхронизированная карта стала похожей на масштабированные упорядоченные карты.

Предсказуемая задержка

Еще одна проблема с подходами взаимного исключения заключается в том, что предположение о полной атомарности, сделанное некоторым однопоточным кодом, создает спорадические недопустимо длительные задержки между потоками в параллельной среде. В частности, итераторы и массовые операции, такие как putAll () и другие, могут занимать время, пропорциональное размеру карты, задерживая другие потоки, ожидающие предсказуемо низкой задержки для не массовых операций. Например, многопоточный веб-сервер не может позволить задерживать некоторые ответы из-за длительных итераций других потоков, выполняющих другие запросы, которые ищут определенное значение. С этим связан тот факт, что потоки, которые блокируют карту, на самом деле не имеют никакого требования когда-либо снимать блокировку, а бесконечный цикл в потоке-владельце может распространять постоянную блокировку на другие потоки. Медленные потоки владельца иногда могут быть прерваны. Карты на основе хеширования также подвержены спонтанным задержкам во время повторного хеширования.

Слабая консистенция

Решение пакетов java.util.concurrency для проблемы одновременной модификации, проблемы с конвоем, проблемы предсказуемой задержки и проблемы с многоядерностью включает архитектурный выбор, называемый слабой согласованностью. Этот выбор означает, что такие операции чтения, как get (), не будут блокироваться, даже когда обновления выполняются, и допустимо даже перекрытие обновлений между собой и с операциями чтения. Слабая согласованность позволяет, например, изменять содержимое ConcurrentMap во время его итерации одним потоком. Итераторы предназначены для использования одним потоком за раз. Так, например, карта, содержащая две взаимозависимые записи, может непоследовательно восприниматься потоком-читателем во время модификации другим потоком. Обновление, которое должно изменить ключ записи (k1, v) к записи (k2, v) атомарно потребуется удалить (k1), а затем положить (k2, v), в то время как итерация может пропустить запись или увидеть ее в двух местах. Получение возвращает значение для данного ключа, которое отражает последний предыдущий завершенный обновление для этого ключа. Таким образом, существует отношение «случилось до».

ConcurrentMaps не может заблокировать всю таблицу. Отсутствует возможность ConcurrentModificationException, как при непреднамеренном одновременном изменении несовпадающих карт. Метод size () может занять много времени, в отличие от соответствующих несовпадающих карт и других коллекций, которые обычно включают поле размера для быстрого доступа, потому что им может потребоваться сканировать всю карту каким-либо образом. Когда происходят одновременные модификации, результаты отражают состояние карты в какой-то момент, но не обязательно одно согласованное состояние, поэтому size (), isEmpty () и containsValue () могут лучше всего использоваться только для мониторинга.

Методы ConcurrentMap 1.5

ConcurrentMap предоставляет некоторые операции, которых нет в Map, которые он расширяет, чтобы обеспечить атомарность модификаций. Заменить (К, v1, v2) будет проверять наличие v1 в записи, обозначенной K и только если найден, то v1 заменяется на v2 атомарно. Новая замена (k, v) сделает пут (k, v) только если k уже есть на карте. Кроме того, putIfAbsent (k, v) сделает пут (k, v) только если k отсутствует на карте, и remove (k, v) удалит запись для v, только если v присутствует. Эта атомарность может быть важной для некоторых вариантов использования с несколькими потоками, но не связана с ограничением слабой согласованности.

Для ConcurrentMaps следующие элементы являются атомарными.

m.putIfAbsent (k, v) атомарен, но эквивалентен:

    если (k == ноль || v == ноль)            бросать новый Исключение нулевого указателя();    если (!м.containsKey(k)) {        возвращаться м.положить(k, v);    } еще {        возвращаться м.получать(k);    }

m replace (k, v) атомарно, но эквивалентно:

    если (k == ноль || v == ноль)            бросать новый Исключение нулевого указателя();    если (м.containsKey(k)) {        возвращаться м.положить(k, v);    } еще {        возвращаться ноль;    }

m.replace (k, v1, v2) атомарен, но эквивалентен:

    если (k == ноль || v1 == ноль || v2 == ноль)            бросать новый Исключение нулевого указателя();     если (м.containsKey(k) && Объекты.равно(м.получать(k), v1)) {        м.положить(k, v2);        возвращаться истинный;     } еще        возвращаться ложный;     }

m.remove (k, v) атомарен, но эквивалентен:

    // если Map не поддерживает нулевые ключи или значения (очевидно, независимо)    если (k == ноль || v == ноль)            бросать новый Исключение нулевого указателя();    если (м.containsKey(k) && Объекты.равно(м.получать(k), v)) {        м.удалять(k);        возвращаться истинный;    } еще       возвращаться ложный;    }

Методы ConcurrentMap 1.8

Поскольку Map и ConcurrentMap являются интерфейсами, к ним нельзя добавлять новые методы без нарушения реализаций. Однако в Java 1.8 добавлена ​​возможность реализации интерфейса по умолчанию и добавлены реализации по умолчанию интерфейса Map некоторых новых методов getOrDefault (Object, V), forEach (BiConsumer), replaceAll (BiFunction), computeIfAbsent (K, Function), computeIfPresent (K , BiFunction), вычислить (K, BiFunction) и объединить (K, V, BiFunction). Реализации по умолчанию в Map не гарантируют атомарность, но в ConcurrentMap, переопределяя значения по умолчанию, они используют Заблокировать бесплатно методы достижения атомарности, а существующие реализации ConcurrentMap автоматически станут атомарными. Безблокировочные методы могут быть медленнее, чем переопределения в конкретных классах, поэтому конкретные классы могут выбирать, реализовывать их атомарно или нет и задокументировать свойства параллелизма.

Атомарность без блокировок

Можно использовать Без блокировки методы с ConcurrentMaps, потому что они включают методы с достаточно высоким согласованным числом, а именно бесконечностью, что означает, что любое количество потоков может быть скоординировано. Этот пример может быть реализован с помощью Java 8 merge (), но он показывает общий шаблон Lock-free, который является более общим. Этот пример не связан с внутренними компонентами ConcurrentMap, а связан с использованием ConcurrentMap клиентским кодом. Например, если мы хотим атомарно умножить значение в Map на константу C:

    статический окончательный длинный C = 10;    пустота atomicMultiply(ConcurrentMap<Длинный, Длинный> карта, Длинный ключ) {        за (;;) {            Длинный oldValue = карта.получать(ключ);            // Предполагается, что oldValue не равно нулю. Это операция «полезная нагрузка», и она не должна иметь побочных эффектов из-за возможного перерасчета при конфликте.            Длинный newValue = oldValue * C;            если (карта.заменять(ключ, oldValue, newValue))                перемена;        }    }

PutIfAbsent (k, v) также полезен, когда разрешено отсутствие записи для ключа. Этот пример может быть реализован с помощью Java 8 compute (), но он показывает общий шаблон Lock-free, который является более общим. Заменить (к, v1, v2) не принимает нулевые параметры, поэтому иногда требуется их комбинация. Другими словами, если v1 имеет значение null, тогда putIfAbsent (k, v2) вызывается, иначе замените (к, v1, v2) вызывается.

    пустота atomicMultiplyNullable(ConcurrentMap<Длинный, Длинный> карта, Длинный ключ) {        за (;;) {            Длинный oldValue = карта.получать(ключ);            // Это операция "полезная нагрузка", и она не должна иметь побочных эффектов из-за возможного перерасчета при конфликте.            Длинный newValue = oldValue == ноль ? ПЕРВОНАЧАЛЬНЫЙ ЗНАЧЕНИЕ : oldValue * C;            если (replaceNullable(карта, ключ, oldValue, newValue))                перемена;        }    }    ...    статический логический replaceNullable(ConcurrentMap<Длинный, Длинный> карта, Длинный ключ, Длинный v1, Длинный v2) {        возвращаться v1 == ноль ? карта.putIfAbsent(ключ, v2) == ноль : карта.заменять(ключ, v1, v2);    }

История

Фреймворк коллекций Java был разработан и разработан в основном Джошуа Блох, и был введен в JDK 1.2.[2] Первоначальные классы параллелизма пришли из Дуг Ли с [3] пакет для сбора.

Смотрите также

Рекомендации

  1. ^ "java.util.Collections.synchronizedMap". Java / Java SE / 11 / API / java.base. Справочный центр Oracle. 19 сентября 2018 г.. Получено 2020-07-17.
  2. ^ Ванхелсуве, Лоуренс (1 января 1999 г.). «Битва контейнерных фреймворков: что лучше использовать?». JavaWorld. Получено 2020-07-17.
  3. ^ Ли, Дуг. "Обзор пакета util.concurrent Release 1.3.4". Получено 2011-01-01.
  • Гетц, Брайан; Джошуа Блох; Джозеф Баубир; Дуг Ли; Дэвид Холмс; Тим Пайерлс (2006). Параллелизм Java на практике. Эддисон Уэсли. ISBN  0-321-34960-1. ПР  25208908M.
  • Ли, Дуг (1999). Параллельное программирование на Java: принципы и шаблоны проектирования. Эддисон Уэсли. ISBN  0-201-31009-0. ПР  55044M.

внешняя ссылка