Мне нужно вычислить хэши SHA-256 в приложении Java. Apache HmacUtils кажется подходящим для этого. Однако в документации говорится: «Этот класс является неизменяемым и потокобезопасным. Однако Mac может быть иным». Здесь «Mac» относится к возвращаемому экземпляру, который фактически вычисляет хэш.
Теперь я хочу иметь возможность вычислять хеши из нескольких потоков, в идеале без их блокировки друг друга. Каков наилучший способ добиться этого? Есть ли ссылка, какой алгоритм на самом деле будет потокобезопасным? Я так понимаю, это тоже должно быть реентерабельно? Должен ли я создавать один экземпляр «Mac» для каждого потока, чтобы избежать проблем с безопасностью потоков? (Достаточно ли этого? Значительно ли это дороже?)
Алгоритмы хеширования по своей сути не являются потокобезопасными, поскольку они управляют состоянием (текущим значением хеш-функции), которое меняется при каждом вызове добавления к нему данных. И вы не хотите смешивать данные, которые вы добавляете в него (т. е. вы не хотите хэшировать 5 байтов из потока 1, затем 10 байтов из потока 2, а затем 27 байтов из потока 3). Один экземпляр Mac может одновременно вычислить только одно значение хеш-функции.
@ThomasKläger «Алгоритмы хеширования по своей сути не являются потокобезопасными». Это совершенно неверно. Скажем, в SHA256 ничего не подразумевается об изменчивости. Его можно реализовать как чистую функцию. Тот факт, что они реализованы изменчиво, не является «присущим» хешированию.
@Майкл, это правда, что алгоритмы хеширования могут быть реализованы как чистые функции. Однако для этого потребуется совершенно другой интерфейс, отличный от того, который Java предоставляет для своих алгоритмов. И вопрос не в том, «можно ли реализовать алгоритмы хеширования как в чистых функциях», а в том, «являются ли реализации, предоставляемые HmacUtils
и Mac
потокобезопасными (т.е. с сохранением состояния или нет)»
@Майкл, вопрос, который я прочитал, касается реализации алгоритмов хеширования в Java. Возможно, мне следовало сформулировать свой первый комментарий так: «Алгоритмы хеширования (реализованные в Java в соответствии со спецификацией криптографии Java) по своей сути не являются потокобезопасными»
@ThomasKläger В этом нет никакого «может быть». Эта формулировка была бы намного лучше. Это гигантский отборочный этап. Я не просто тупой. Это техническая дискуссия, где точность имеет значение. В противном случае вы просто запутаете людей.
Потокобезопасность — это неправильная терминология, но криптографические хэш-функции не являются коммутативными. Разговоры о хешировании (или MAC-кодировании) в нескольких потоках — это как минимум тревожный сигнал.
«Это значительно дороже?» Дороже чем что? Если вам приходится использовать один экземпляр на поток, а вы это делаете, вам не с чем его сравнивать, поэтому «дороже» не имеет смысла.
Есть несколько способов настроить это:
Другими словами, вы создаете новый экземпляр 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, который является более кратким.
ответ отредактирован.
Большинство алгоритмов хеширования, которые мы используем сегодня, работают путем последовательной обработки фрагментов данных (обычно 512 или 1024 бита). Это справедливо для SHA-2, SHA-3 и непараллельных вариантов BLAKE2, которые являются наиболее распространенными безопасными алгоритмами хэширования, используемыми сегодня. То же самое относится и к конструкциям HMAC или встроенным конструкциям MAC, основанным на этих алгоритмах.
В результате вам, как правило, все равно потребуется обрабатывать экземпляр этих алгоритмов в одном потоке, поскольку сами алгоритмы не поддаются распараллеливанию. Использование типичного, нечистого подхода к реализации алгоритмов хэширования (подход, который использует почти каждая криптографическая библиотека, включая эту), означает, что MAC или экземпляр хэша являются изменяемыми и должны использоваться либо в одном потоке, либо синхронизироваться. по потокам. Как я уже упоминал, обычно первое является лучшей идеей.
Стоимость создания экземпляра хеша или MAC обычно очень мала, так что это практически не проблема. Однако если вы выполняете множество MAC-адресов с одним и тем же ключом, может быть несколько более эффективно создать один экземпляр с инициализированным ключом, а затем клонировать его, что экономит затраты на хеширование первого блока для ключа. Это может быть значительным улучшением производительности, если сообщение небольшое.
Существуют алгоритмы, такие как BLAKE3 и MD6, которые используют хеширование деревьев, и они часто могут выиграть от распараллеливания. Обычно у них есть документация, в которой рассказывается, как создаются и обрабатываются потоки (часто с использованием пула потоков), а также соответствующие требования к безопасности потоков. Эти алгоритмы часто имеют внутреннюю поточность, поэтому экземпляры можно безопасно использовать в разных потоках, если это указано в документации. Однако используемая вами библиотека не предлагает ничего из этого, так что вам не придется с этим сталкиваться.
Если вы планируете обрабатывать небольшие порции данных, вам почти наверняка захочется использовать пул потоков, поскольку накладные расходы на создание потока могут быть нетривиальными по сравнению с хешированием.
Спасибо. Я работаю с приложением Spring, которое уже заботится о пуле потоков. Мне до сих пор неясно следующее: что делает HmacUtils::hmacHex? В нем говорится, что HmacUtils защищен от угроз. Итак, я бы предположил, что использование одного экземпляра HmacUtils в нескольких потоках — это нормально?
В документации есть пример, где написано «Повторное использование Mac», где HmacUtils::hmacHex используется дважды. Как это может быть повторное использование Mac, если HmacUtils::hmacHex должен быть потокобезопасным (в документах указано, что HmacUtils IS потокобезопасен)?
Это потокобезопасно, поскольку hmacHex
— это одноразовая функция. Другими словами, он принимает все данные сразу и инкапсулирует любое изменяемое состояние внутри самой функции, не изменяя внешнюю структуру Mac
. В других реализациях также возможно постепенно принимать данные посредством нескольких вызовов функций, и в этом случае у вас есть изменяемое состояние, которое может быть небезопасным для потоков.
Я взглянул на код (github.com/apache/commons-codec/blob/master/src/main/java/org/…). По крайней мере, для аргумента Inputsteam код HmacUtils::hmac, похоже, не поддерживает сохранение потоков. Было бы так, если бы он создал локальный экземпляр Mac. Но он использует частное поле «mac». Поэтому я ожидаю, что вызов этой функции из двух потоков с использованием одного экземпляра HmacUtils будет работать неправильно. Но тогда почему в документации сказано, что HmacUtils поддерживает сохранение потоков?
«Следует ли мне создавать один экземпляр «Mac» для каждого потока» Да «Это значительно дороже?» Нет смысла спекулировать. Измерьте его и посмотрите результаты, если вас это беспокоит. Скорее всего, нет.