1: Что такое «неблокирующий» параллелизм и чем он отличается от обычного параллелизма с использованием потоков? Почему мы не используем неблокирующий параллелизм во всех сценариях, где требуется параллелизм? Есть ли накладные расходы на использование неблокирующего параллелизма?
Не блокирующие алгоритмы не используют конкретные схемы блокировки объектов для управления одновременным доступом к памяти (синхронизированные и стандартные блокировки объектов являются примерами, которые используют блокировки уровня объекта / функции для уменьшения проблем одновременного доступа в Java. Вместо этого они используют некоторую форму низкого уровня инструкция для выполнения (на некотором уровне) одновременного сравнения и обмена в ячейке памяти, если это не удается, оно просто возвращает false и не выдает ошибку, если это работает, то это было успешно, и вы продолжаете. Обычно это делается в Цикл, пока он не будет работать, так как, когда это не удастся, будут только небольшие периоды времени (надеюсь), он просто зацикливается еще несколько раз, пока не сможет установить необходимую память.
Это не всегда используется, потому что это намного сложнее с точки зрения кода даже для относительно тривиальных случаев использования, чем стандартная синхронизация Java. Более того, для большинства применений влияние блокировки на производительность является тривиальным по сравнению с другими источниками в системе. В большинстве случаев требования к производительности недостаточно высоки, чтобы на это можно было даже смотреть.
Наконец, по мере развития JDK / JRE разработчики ядра улучшают реализацию на внутреннем языке, пытаясь включить наиболее эффективные средства достижения этих целей в конструкции ядра. По мере удаления от базовых конструкций вы теряете автоматическую реализацию этих улучшений, поскольку вы используете менее стандартные реализации (например, jaxb / jibx; jaxb раньше сильно уступал jibx, но теперь равен, если не быстрее, в большинстве случаев, что я проверено на Java 7), когда вы подняли свою версию Java.
если вы посмотрите на пример кода ниже, вы можете увидеть «накладные» места. Это на самом деле не накладные расходы, но код должен быть чрезвычайно эффективным, чтобы работать без блокировки, и на самом деле работать лучше, чем стандартная синхронизированная версия из-за зацикливания. Даже небольшие изменения могут привести к тому, что код будет работать в несколько раз лучше, чем стандарт, в код, который в несколько раз хуже (например, экземпляры объектов, которые не нужны, или даже быстрые условные проверки; вы говорите о сохранении циклов). здесь, поэтому разница между успехом и провалом очень мала).
2: я слышал, что неблокирующий параллелизм доступен в Java. Есть ли какие-либо конкретные сценарии, в которых мы должны использовать эту функцию?
По моему мнению, вы должны использовать это только в том случае, если у вас: A) есть доказанная проблема производительности в вашей работающей системе на производстве, на ее производственном оборудовании; и B) если вы можете доказать, что единственная неэффективность, оставшаяся в критической секции, связана с блокировкой; C) Вы твердо согласны с заинтересованными сторонами, что они готовы иметь нестандартный, менее обслуживаемый код в обмен на повышение производительности, которое вы должны D) численно доказать на своем производственном оборудовании, чтобы быть уверенным, что оно вообще поможет.
3: Есть ли разница или преимущество в использовании одного из этих методов с коллекцией? Каковы компромиссы?
Преимущество - производительность, компромисс в первую очередь заключается в том, что это более специализированный код (так что многие разработчики не знают, что с ним делать; усложняет задачу для новой команды или нового найма, помните, что большая часть стоимости программного обеспечения - это рабочая сила, поэтому вы должны следить за общей стоимостью владения, которую вы налагаете на проектные решения), и что любые модификации должны быть проверены снова, чтобы убедиться, что конструкция все еще на самом деле быстрее. Как правило, в системе, которая потребует этого, для любых изменений потребуется тестирование производительности или нагрузки и пропускной способности. Если вы не выполняете эти тесты, то я бы сказал, что вам почти наверняка не нужно даже думать об этих подходах, и вы почти наверняка не увидите какой-либо ценности для повышенной сложности (если вы все сделали правильно).
Опять же, я просто должен переформулировать все стандартные предупреждения против оптимизации в целом, так как многие из этих аргументов те же, что я использовал бы против этого в качестве дизайна. Многие из недостатков этого аналогичны любой оптимизации, например, всякий раз, когда вы меняете код, вы должны убедиться, что ваше «исправление» не приводит к неэффективности в какой-то конструкции, которая была размещена там только для повышения производительности, и иметь дело с это (то есть до рефакторинга всего раздела для возможного удаления оптимизаций), если исправление критично и снижает производительность.
Это действительно, очень легко испортить это способами, которые очень сложно отлаживать, так что если вам не нужно это делать (что я нашел только в нескольких сценариях, где вы когда-либо делали бы, и для меня это были довольно сомнительными, и я бы предпочел не делать этого) не делать этого. используйте стандартные вещи, и все будут счастливы!
Обсуждение / Код
Неблокирующий или свободный от блокировок параллелизм позволяет избежать использования определенных блокировок объектов для управления доступом к общей памяти (например, синхронизированных блоков или определенных блокировок). Есть преимущество в производительности, когда секция кода не блокируется; тем не менее, код в цикле CAS (если вы идете по этому пути, есть другие методы в Java) должен быть очень, очень эффективным, или это в конечном итоге будет стоить вам больше производительности, чем вы получаете.
Как и все оптимизации производительности, дополнительная сложность не стоит эффекта для большинства случаев использования. Чисто написанная Java с использованием стандартных конструкций будет работать, если не лучше, чем большинство оптимизаций (и фактически позволит вашей организации легче поддерживать программное обеспечение после вашего ухода). На мой взгляд, это имеет смысл только в разделах с очень высокой производительностью и проверенными проблемами производительности, где блокировка является единственным источником неэффективности. Если у вас точно нет известной и количественной проблемы с производительностью, я бы избегал использования любой подобной методики, пока вы не докажете, что проблема действительно существует из-за блокировки, и не затрагивает другие проблемы с эффективностью кода. Если у вас есть проверенная проблема с производительностью на основе блокировок, я гарантирую, что у вас есть какой-то тип метрики, чтобы гарантировать, что этот тип установки будет работать быстрее для вас, чем просто использование стандартного параллелизма Java.
Реализация, которую я сделал для этого, использует операции CAS и семейство переменных Atomic. Этот базовый код никогда не блокировался и не вызывал каких-либо ошибок для меня в этом случае использования (ввод и вывод случайной выборки для автономного тестирования из системы перевода с высокой пропускной способностью). В основном это работает так:
У вас есть некоторый объект, который совместно используется потоками, и он объявляется либо как AtomicXXX, либо как AtomicReference (для большинства нетривиальных случаев использования вы будете работать с версией AtomicReference).
когда на указанное значение / объект ссылаются, вы извлекаете его из оболочки Atomic, это дает вам локальную копию, для которой вы выполняете некоторую модификацию. Отсюда вы используете compareAndSwap в качестве условия цикла while, чтобы попытаться установить этот Atomic из вашего потока, если в случае неудачи он возвращает false, а не блокирует. Это будет повторяться до тех пор, пока не заработает (код в этом цикле должен быть очень эффективным и простым).
Вы можете посмотреть на операции CAS, чтобы увидеть, как они работают, в основном они предназначены для реализации в виде единого набора инструкций со сравнением в конце, чтобы увидеть, соответствует ли значение тому, которое вы пытались установить.
Если CompareAndSwap завершается неудачно, вы снова получаете свой объект из оболочки Atomic, снова выполняете любые изменения, а затем снова пытаетесь сравнить и поменять местами, пока он не заработает. Конкретной блокировки нет, вы просто пытаетесь вернуть объект обратно в память, и если он не работает, попробуйте еще раз, когда ваш поток снова получает контроль.
Код для этого ниже для простого случая со списком:
/* field declaration*/
//Note that I have an initialization block which ensures that the object in this
//reference is never null, this was required to remove null checks and ensure the CAS
//loop was efficient enough to improve performance in my use case
private AtomicReference<List<SampleRuleMessage>> specialSamplingRulesAtomic = new AtomicReference<List<SampleRuleMessage>>();
/*start of interesting code section*/
List<SampleRuleMessage> list = specialSamplingRulesAtomic.get();
list.add(message);
while(!specialSamplingRulesAtomic.compareAndSet(specialSamplingRulesAtomic.get(), list)){
list = specialSamplingRulesAtomic.get();
list.add(message);
};
/* end of interesting code section*/