Как выстрелить себе в ногу с помощью генератора случайных чисел

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

3К открытий3К показов

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

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

Для начала давайте посмотрим, как класс java.util.Random работает при использовании нескольких потоков. Его объекты потокобезопасны, но, тем не менее, одновременное использование одного и того же объекта этого класса в разных потоках синхронизировано. Как мы выяснили, это зачастую приводит к значительному замедлению работы приложения из-за того, что несколько потоков пытаются одновременно получить доступ к одному и тому же ресурсу.

Поначалу это может показаться не самой насущной проблемой. Как часто вам требуется сделать что-то непредсказуемое в вашем корпоративном приложении? Наоборот, необходимо следовать жестко заданным бизнес-правилам. Следует признать, что иногда эти так называемые «правила» еще более случайны, чем генераторы по-настоящему рандомных чисел, но это уже совсем другая история…

Как всегда, дьявол кроется в мелочах, и в данном случае это подкласс java.util.Randomjava.util.SecureRandom. Как следует из его названия, он применяется, если результат работы генератора должен быть криптографически стойким. По никому неизвестным причинам объекты этого класса используются в ситуациях, где криптографическая стойкость, казалось бы, не имеет особого значения.

Мы заметили эту неприятную особенность, когда разрабатывали инструмент для автоматического обнаружения проблем взаимодействия потоков и блокировок. Судя по результатам проделанной нами работы, одна из самых частых проблем возникает при вызове безобидно выглядящего метода 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;      // крайний случай
    } else {
        n = Math.abs(n);
    }
    return new File(dir, prefix + Long.toString(n) + suffix);
}
		

При вызове nextLong() объект класса SecureRandom прибегает к своему методу nextBytes(), который объявлен как синхронизированный:

			synchronized public void nextBytes(byte[] bytes) {
    secureRandomSpi.engineNextBytes(bytes);
}
		

Можно сказать, что если создавать новый экземпляр SecureRandom в каждом потоке, то никаких проблем не возникнет. К сожалению, не все так просто. SecureRandom применяет абстрактный класс java.security.SecureRandomSpi, и все экземпляры будут обращаться к нему, так что затруднений не избежать (здесь ведется обсуждение этого бага, там же можно найти тесты производительности при нем).

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

Исправить ситуацию в случае, если вы можете изменять код приложения, легко — достаточно просто использовать java.util.ThreadLocalRandom в условиях многопоточности. Однако может случиться и так, что проблема кроется в стандартном API — тогда серьезного рефакторинга не избежать.

В чем мораль сей истории? Работать с потоками следует осторожно. Особенно если ваши инструменты для разработки это не учитывают. В любом случае, надеемся, что эта статья предотвратит появление библиотек, где генерация случайных чисел станет камнем преткновения.

 Перевод статьи «Shooting yourself in foot with Random number generators»

Следите за новыми постами
Следите за новыми постами по любимым темам
3К открытий3К показов