Это не будет одним из постов, объясняющих, как генератор случайных чисел не так уж и случайен. Таким образом, те из вас, кто ожидает указания о том, как взломать игровой автомат, двигаться вперед, здесь ничего не видно.
Вместо этого это пост об одной из не так уж и необычных проблем , связанных с конфликтами блокировок , скрытых в генераторах случайных чисел в API Java.
Чтобы открыть тему, давайте начнем с рассмотрения того, как обрабатывается параллелизм в классе java.util.Random . Экземпляры java.util.Random являются поточно-ориентированными. Однако одновременное использование одного и того же экземпляра java.util.Random между потоками синхронизируется, и, как мы выяснили, имеет тенденцию вызывать проблемы конкуренции, влияющие на производительность приложения.
В вашем обычном повседневном корпоративном приложении это может показаться не важной проблемой — в конце концов, как часто вы на самом деле делаете что-то заведомо непредсказуемое? Вместо этого вы все о предсказуемо следуя бизнес-правилам. Я должен признать, однако, что в некоторых случаях эти бизнес-правила имеют тенденцию включать даже больше энтропии, чем алгоритм действительно случайного начального поколения, но это будет совсем другая история.
Но дьявол скрыт в деталях, которые в данном случае являются подклассом java.util.Random , а именно java.util.SecureRandom . Этот класс в качестве состояний имен следует использовать в тех случаях, когда результат генератора случайных чисел должен быть криптографически безопасным. По неизвестным человечеству причинам эта реализация была выбрана в качестве основы во многих распространенных API в ситуациях, когда обычно не следует ожидать, что криптографически безопасные аспекты случайности будут иметь значение.
Мы столкнулись с проблемой воочию, пристально следя за принятием решения по обнаружению конфликтов блокировки . Основываясь на результатах, одна из наиболее распространенных проблем блокировки в приложениях Java запускается через невинно выглядящие вызовы java.io.File.createTempFile () . В основе этого временного создания файла лежит класс SecureRandom для вычисления имени файла.
01
02
03
04
05
06
07
08
09
10
|
private static final SecureRandom random = new SecureRandom(); static File generateFile(String prefix, String suffix, File dir) { long n = random.nextLong(); if (n == Long.MIN_VALUE) { n = 0 ; // corner case } else { n = Math.abs(n); } return new File(dir, prefix + Long.toString(n) + suffix); } |
И SecureRandom, когда вызывается nextLong, в конце концов вызывает свой метод nextBytes () , который определен как синхронизированный:
1
2
3
|
synchronized public void nextBytes( byte [] bytes) { secureRandomSpi.engineNextBytes(bytes); } |
Можно сказать, что если я создам новый SecureRandom в каждом потоке, у меня не возникнет никаких проблем. К сожалению, не все так просто. SecureRandom использует реализацию java.security.SecureRandomSpi , которая в конечном итоге все равно будет оспорена (вы можете посмотреть следующее обсуждение ошибки с некоторыми тестами в трекере проблем Jenkins )
Это в сочетании с определенными шаблонами использования приложений (особенно если у вас много SSL-соединений, использующих SecureRandom для их крипто-рукопожатия), имеет тенденцию накапливаться в длительных конфликтных ситуациях.
Исправить ситуацию просто, если вы можете контролировать исходный код — просто пересоберите решение, чтобы использовать многопоточный дизайн на основе java.util.ThreadLocalRandom . В случаях, когда вы застряли со стандартным API, принимающим решения за вас, решение может быть более сложным и требовать значительного рефакторинга.
Мораль истории? Параллельность это сложно. Особенно, когда строительные блоки вашей системы не приняли это во внимание. В любом случае, я надеюсь, что статья спасет мир, по крайней мере, от пары новых библиотек, в которых генераторы случайных чисел станут предметом спора.
Ссылка: | Снимайте ноги с помощью генераторов случайных чисел от нашего партнера JCG Владимира Сора в блоге Plumbr Blog . |