Какие реализации алгоритмов хэширования, таких как SHA-256 для Java, являются потокобезопасными?

Мне нужно вычислить хэши SHA-256 в приложении Java. Apache HmacUtils кажется подходящим для этого. Однако в документации говорится: «Этот класс является неизменяемым и потокобезопасным. Однако Mac может быть иным». Здесь «Mac» относится к возвращаемому экземпляру, который фактически вычисляет хэш.

Теперь я хочу иметь возможность вычислять хеши из нескольких потоков, в идеале без их блокировки друг друга. Каков наилучший способ добиться этого? Есть ли ссылка, какой алгоритм на самом деле будет потокобезопасным? Я так понимаю, это тоже должно быть реентерабельно? Должен ли я создавать один экземпляр «Mac» для каждого потока, чтобы избежать проблем с безопасностью потоков? (Достаточно ли этого? Значительно ли это дороже?)

«Следует ли мне создавать один экземпляр «Mac» для каждого потока» Да «Это значительно дороже?» Нет смысла спекулировать. Измерьте его и посмотрите результаты, если вас это беспокоит. Скорее всего, нет.

Michael 07.05.2024 16:31

Алгоритмы хеширования по своей сути не являются потокобезопасными, поскольку они управляют состоянием (текущим значением хеш-функции), которое меняется при каждом вызове добавления к нему данных. И вы не хотите смешивать данные, которые вы добавляете в него (т. е. вы не хотите хэшировать 5 байтов из потока 1, затем 10 байтов из потока 2, а затем 27 байтов из потока 3). Один экземпляр Mac может одновременно вычислить только одно значение хеш-функции.

Thomas Kläger 07.05.2024 16:49

@ThomasKläger «Алгоритмы хеширования по своей сути не являются потокобезопасными». Это совершенно неверно. Скажем, в SHA256 ничего не подразумевается об изменчивости. Его можно реализовать как чистую функцию. Тот факт, что они реализованы изменчиво, не является «присущим» хешированию.

Michael 07.05.2024 17:12

@Майкл, это правда, что алгоритмы хеширования могут быть реализованы как чистые функции. Однако для этого потребуется совершенно другой интерфейс, отличный от того, который Java предоставляет для своих алгоритмов. И вопрос не в том, «можно ли реализовать алгоритмы хеширования как в чистых функциях», а в том, «являются ли реализации, предоставляемые HmacUtils и Mac потокобезопасными (т.е. с сохранением состояния или нет)»

Thomas Kläger 07.05.2024 17:26

@Майкл, вопрос, который я прочитал, касается реализации алгоритмов хеширования в Java. Возможно, мне следовало сформулировать свой первый комментарий так: «Алгоритмы хеширования (реализованные в Java в соответствии со спецификацией криптографии Java) по своей сути не являются потокобезопасными»

Thomas Kläger 07.05.2024 18:02

@ThomasKläger В этом нет никакого «может быть». Эта формулировка была бы намного лучше. Это гигантский отборочный этап. Я не просто тупой. Это техническая дискуссия, где точность имеет значение. В противном случае вы просто запутаете людей.

Michael 07.05.2024 18:11

Потокобезопасность — это неправильная терминология, но криптографические хэш-функции не являются коммутативными. Разговоры о хешировании (или MAC-кодировании) в нескольких потоках — это как минимум тревожный сигнал.

President James K. Polk 07.05.2024 18:28

«Это значительно дороже?» Дороже чем что? Если вам приходится использовать один экземпляр на поток, а вы это делаете, вам не с чем его сравнивать, поэтому «дороже» не имеет смысла.

user207421 07.05.2024 19:00
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
3
8
153
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Есть несколько способов настроить это:

Вы создаете тему для каждой имеющейся у вас работы.

Другими словами, вы создаете новый экземпляр java.lang.Thread (или экземпляр класса, который extends Thread), который, если вы его запустите, выполнит одну работу, а затем завершится.

Это плохая идея — по крайней мере, если важна скорость и вам предстоит выполнить массу работы. Фактическая стоимость создания этих потоков зависит от множества факторов, поистине комбинационного взрыва: версии JVM, поставщика, базовой ОС, базовой архитектуры. Вполне возможно, что это хороший способ сделать это с точки зрения скорости, но есть комбинации, в которых это намного медленнее, чем альтернатива, заключающаяся в ограниченном наборе потоков (примерно равном количеству ядер в вашем процессоре, возможно, примерно в 2 раза больше, но линейно зависит от количества ядер), и эти потоки извлекают задания из центральной очереди em до тех пор, пока все задания не будут выполнены.

Существуют различные фреймворки, которые делают это «хорошим» (например, fork/join, ExecutorPool и т. д.), и если вы каким-то образом хотите управлять всем этим, вы можете использовать типы коллекций в java.util.concurrent для «центральной очереди всех выполняемых заданий». из которого тянутся все нити».

Дело в том, что беспокоиться о затратах, связанных с созданием нового экземпляра объекта Mac для каждого задания (что, по сути, и есть то, что вам придется в конечном итоге делать, чтобы избежать каких-либо проблем с потокобезопасностью), довольно глупо. если ты не обратился к слону в комнате. Другими словами, если это описывает вашу текущую настройку, сначала исправьте ее, чтобы в итоге получилось:

У вас есть рабочие места и пул исполнителей, которые получают от них рабочие места.

... и количество исполнителей ограничено (того же порядка, что и количество ядер ЦП в машине).

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

Тогда решение состоит в том, чтобы просто создать 1 объект Mac для каждого потока. Это означает, что у вас есть 1 объект Mac на каждый процессор, что является тривиальной и игнорируемой стоимостью. Нет смысла пытаться использовать меньше объектов Mac; вы не получите измеримой производительности, даже если бы это было нормально (т. е. объекты Mac вообще не имеют контекста, что, очевидно, неверно), или даже потеряете производительность, если каким-то образом эти объекты Mac являются потокобезопасными, но имеют состояние (потому что JVM очень хорошо устраняет затраты на синхронизацию, если они не имеют значения, поскольку объект никогда не используется из нескольких потоков, даже если бы это сработало, если бы вы это сделали).

Одним из инструментов для достижения этой цели является наличие ThreadLocal:

private static final ThreadLocal<Mac> MACS = ThreadLocal.withInitial(
  () -> HmacUtils.getInitializedMac("algorithm", key));


public void codeThatIsRunInManyThreads() {
  Mac mac = MACS.get();
  mac.reset();
  // use mac here. Rest safely knowing it's all safe now.
}

Этот код гарантирует, что каждый поток создаст новый объект Mac, но сделает это один раз — этот созданный объект Mac будет повторно использоваться для каждого задания. Создается столько же объектов Mac, сколько потоков, в конечном итоге выполняющих хотя бы одно задание. 32 ядра процессора и 10 000 рабочих мест? Будет создано около 64 объектов Mac.

Каждый объект Mac изолирован в одном потоке. Если это не потокобезопасно, не проблема. Если это так, то synchronized или другие механизмы безопасности потоков, которые он использует, не требуют больших затрат, поскольку JVM сможет довольно легко определить, что они ничего не делают, и устранить их.

Как только потоки умрут (что произойдет после завершения всех заданий), созданные объекты Mac будут собирать мусор так же, как и любой мусор (поэтому они этого не сделают, пока не пройдет много времени или не потребуется память - как и весь мусор) .

Создание подкласса ThreadLocal не требуется, начиная с Java 8. Существует ThreadLocal::withInitial, который является более кратким.

Michael 07.05.2024 17:16

ответ отредактирован.

rzwitserloot 07.05.2024 18:09
Ответ принят как подходящий

Большинство алгоритмов хеширования, которые мы используем сегодня, работают путем последовательной обработки фрагментов данных (обычно 512 или 1024 бита). Это справедливо для SHA-2, SHA-3 и непараллельных вариантов BLAKE2, которые являются наиболее распространенными безопасными алгоритмами хэширования, используемыми сегодня. То же самое относится и к конструкциям HMAC или встроенным конструкциям MAC, основанным на этих алгоритмах.

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

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

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

Если вы планируете обрабатывать небольшие порции данных, вам почти наверняка захочется использовать пул потоков, поскольку накладные расходы на создание потока могут быть нетривиальными по сравнению с хешированием.

Спасибо. Я работаю с приложением Spring, которое уже заботится о пуле потоков. Мне до сих пор неясно следующее: что делает HmacUtils::hmacHex? В нем говорится, что HmacUtils защищен от угроз. Итак, я бы предположил, что использование одного экземпляра HmacUtils в нескольких потоках — это нормально?

Falk Tandetzky 10.05.2024 14:08

В документации есть пример, где написано «Повторное использование Mac», где HmacUtils::hmacHex используется дважды. Как это может быть повторное использование Mac, если HmacUtils::hmacHex должен быть потокобезопасным (в документах указано, что HmacUtils IS потокобезопасен)?

Falk Tandetzky 10.05.2024 14:31

Это потокобезопасно, поскольку hmacHex — это одноразовая функция. Другими словами, он принимает все данные сразу и инкапсулирует любое изменяемое состояние внутри самой функции, не изменяя внешнюю структуру Mac. В других реализациях также возможно постепенно принимать данные посредством нескольких вызовов функций, и в этом случае у вас есть изменяемое состояние, которое может быть небезопасным для потоков.

bk2204 10.05.2024 23:16

Я взглянул на код (github.com/apache/commons-codec/blob/master/src/main/java/o‌​rg/…). По крайней мере, для аргумента Inputsteam код HmacUtils::hmac, похоже, не поддерживает сохранение потоков. Было бы так, если бы он создал локальный экземпляр Mac. Но он использует частное поле «mac». Поэтому я ожидаю, что вызов этой функции из двух потоков с использованием одного экземпляра HmacUtils будет работать неправильно. Но тогда почему в документации сказано, что HmacUtils поддерживает сохранение потоков?

Falk Tandetzky 27.05.2024 15:10

Другие вопросы по теме