Автор Владимир Шор для Plumbr .
Это не будет одним из постов, объясняющих, как генератор случайных чисел не так уж и случайен. Таким образом, те из вас, кто ожидает указания о том, как взломать игровой автомат, двигаться вперед, здесь ничего не видно.
Вместо этого это пост об одной из не так уж и необычных проблем , связанных с конфликтами блокировок , скрытых в генераторах случайных чисел в API Java.
Чтобы открыть тему, давайте начнем с рассмотрения того, как обрабатывается параллелизм в классе java.util.Random . Экземпляры java.util.Random являются поточно-ориентированными. Однако одновременное использование одного и того же экземпляра java.util.Random между потоками синхронизируется, и, как мы выяснили, имеет тенденцию вызывать проблемы конкуренции, влияющие на производительность приложения.
В вашем обычном повседневном корпоративном приложении это может показаться не важной проблемой — в конце концов, как часто вы на самом деле делаете что-то заведомо непредсказуемое? Вместо этого вы все о предсказуемо следуя бизнес-правилам. Я должен признать, однако, что в некоторых случаях эти бизнес-правила имеют тенденцию включать даже больше энтропии, чем алгоритм действительно случайного начального поколения, но это будет совсем другая история.
Но дьявол скрыт в деталях, которые в данном случае являются подклассом java.util.Random , а именно java.util.SecureRandom . Этот класс, как и имена состояний, следует использовать в тех случаях, когда результат генератора случайных чисел должен быть криптографически безопасным. По неизвестным человечеству причинам эта реализация была выбрана в качестве основы во многих распространенных API в ситуациях, когда обычно не следует ожидать, что криптографически безопасные аспекты случайности будут иметь значение.
Мы столкнулись с проблемой воочию, пристально следя за принятием решения по обнаружению конфликтов блокировки . Основываясь на результатах, одна из наиболее распространенных проблем блокировки в приложениях Java запускается через невинно выглядящие вызовы java.io.File.createTempFile () . Под капотом, это создание временного файла полагаясь на SecureRandom класс для вычисления имени файла.
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 () , который определен как синхронизированный:
synchronized public void nextBytes(byte[] bytes) { secureRandomSpi.engineNextBytes(bytes); }
Можно сказать, что если я создам новый SecureRandom в каждом потоке, у меня не возникнет никаких проблем. К сожалению, не все так просто. SecureRandom использует реализацию java.security.SecureRandomSpi , которая в конечном итоге все равно будет оспорена (вы можете посмотреть следующее обсуждение ошибки с некоторыми тестами в трекере проблем Jenkins )
Это в сочетании с определенными шаблонами использования приложений (особенно если у вас много SSL-соединений, использующих SecureRandom для их крипто-рукопожатия), имеет тенденцию накапливаться в длительных конфликтных ситуациях.
Исправить ситуацию просто, если вы можете контролировать исходный код — просто пересоберите решение, чтобы использовать многопоточный дизайн на основе java.util.ThreadLocalRandom . В случаях, когда вы застряли со стандартным API, принимающим решения за вас, решение может быть более сложным и требовать значительного рефакторинга.
Мораль истории? Параллельность это сложно. Особенно, когда строительные блоки вашей системы не приняли это во внимание. В любом случае, я надеюсь, что статья спасет мир, по крайней мере, от пары новых библиотек, где генераторы случайных чисел станут предметом спора.