Статьи

Съемка себя в ногу с генераторами случайных чисел

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

Вместо этого это пост об одной из не так уж и необычных проблем , связанных с конфликтами блокировок , скрытых в генераторах случайных чисел в 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, принимающим решения за вас, решение может быть более сложным и требовать значительного рефакторинга.

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