Исследование случайных чисел Java

Java

1. Введение

Когда речь заходит о случайных числах в Java, многие думают оRandom, когда есть необходимость сгенерировать случайные числа, большинство людей предпочтут использовать Random для генерации случайных чисел. Класс Random является потокобезопасным, но для обеспечения безопасности потоков он использует CAS внутри себя.В случае многопоточного параллелизма его производительность можно оптимизировать. После JDK1.7 Java предоставляет лучшее решение ThreadLocalRandom.Далее давайте обсудим, чем отличаются реализации этих генераторов случайных чисел.

2 Random

Класс Random — это класс, предоставляемый JDK для генерации случайных чисел.Этот класс на самом деле не случайный, а псевдослучайный.Псевдослучайный означает, что сгенерированные случайные числа на самом деле имеют определенные правила, и этот закон проявляется Период зависит от плюсов минусы псевдослучайного алгоритма Вообще говоря, период относительно большой, но его можно предсказать. Мы можем просто использовать Random со следующим кодом:

img

Случайный принцип

В Random много методов, здесь мы разберем более распространенные методы nextInt() и nextInt(intbound) Первый будет вычислять случайное число в диапазоне int, и если мы перейдем в 10, то он найдет [ A случайное число типа int между 0,10), слева закрыто и справа открыто. Давайте сначала посмотрим на конструктор Random():

img

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

nextInt()

Код для nextInt() выглядит так:

img

В нем напрямую вызывается метод next(), а входящие 32 относятся к числу битов типа Int.

img

Здесь по текущему значению начального числа будет вычисляться следующее начальное число по определенным правилам (псевдослучайный алгоритм), а затем будет выполняться CAS, при сбое CAS вышеперечисленные операции будут продолжать циклически повторяться. Наконец, вернитесь в соответствии с количеством битов, которое нам нужно. Ядром является алгоритм CAS.

nextInt(int bound)

Код для nextInt(intbound) выглядит так:

img

Этот процесс на несколько шагов больше, чем nextInt().Конкретные шаги следующие:

  1. Сначала получите 31-битное случайное число, обратите внимание, что это 31 бит, что отличается от 32 бит выше, потому что случайное число, которое может быть получено в методе nextInt(), может быть отрицательным, а nextInt(intbound) обуславливает что только [0,bound) перед случайным числом, а это значит, что оно должно быть положительным числом, а бит знака зарезервирован, поэтому получается только 31 бит. (Не думайте об использовании абсолютного значения этой операции, это приведет к снижению производительности)
  2. Затем выполните связанную операцию.
  3. Если привязка является степенью числа 2, вы можете напрямую умножить значение, полученное на первом шаге, на привязку, а затем сдвинуть вправо на 31 бит.Объясните: если привязка равна 4, то умножение на 4 на самом деле является сдвигом влево на 2 бита, который на самом деле изменяется на 33 бита, а затем на 31 бит вправо, он снова станет 2-битным, и, наконец, диапазон 2-битного int на самом деле [0,4].
  4. Если это не степень числа 2, оно обрабатывается по модулю.

Узкое место параллелизма

В моих предыдущих статьях есть соответствующие введения.Вообще говоря, CAS имеет определенные преимущества перед блокировкой, но это не обязательно означает эффективность. Немедленная мысль о решении состоит в том, чтобы создавать новый потоко-частный объект Random каждый раз, когда вы используете Random, или использовать ThreadLocal для поддержки потоково-частного объекта, но есть более эффективные решения, следующие. Давайте представим главного героя этой статьи. , ThreadLocalRandom.

3 ThreadLocalRandom

После JDK1.7 предоставляется новый класс ThreadLocalRandom для замены Random в одновременных сценариях. Метод использования относительно прост:

ThreadLocalRandom.current().nextInt();
ThreadLocalRandom.current().nextInt(10);

В текущем методе есть:

img
Вы можете видеть, что он будет инициализирован, если он не инициализирован, и здесь наш seed больше не является глобальной переменной, в нашем Thread есть три переменных:
img

  • threadLocalRandomSeed: используется ThreadLocalRandom для управления начальным числом случайных чисел.
  • threadLocalRandomProbe: используется ThreadLocalRandom для управления инициализацией.
  • threadLocalRandomSecondarySeed: вторичное семя.

Вы можете видеть, что все переменные помечены @sun.misc.Contended для борьбы с ложным разделением.

Код в методе nextInt() выглядит следующим образом:

img

Код нашего ключа выглядит следующим образом:

UNSAFE.putLong(t = Thread.currentThread(), SEED,r=UNSAFE.getLong(t, SEED) + GAMMA);

Видно, что поскольку каждый поток поддерживает свое собственное начальное число, CAS в это время не требуется, а put выполняется напрямую, а изоляция между потоками здесь используется для уменьшения конфликтов параллелизма; по сравнению сThreadLocal<Random>, ThreadLocalRandom не только снижает стоимость обслуживания объекта, но и упрощает его внутреннюю реализацию. Таким образом, ThreadLocalRandom имеет высокую производительность.

4 Тест производительности

В дополнение к Random, ThreadLocalRandom, подробно описанным в статье, я также реализую ThreadLocalRandom, реализованный netty4, иThreadLocal<Random>В качестве эталонного объекта примите участие в оценке JMH вместе.

@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 3, time = 5)
@Threads(50)
@Fork(1)
@State(Scope.Benchmark)
public class RandomBenchmark {

    Random random = new Random();

    ThreadLocal<Random> threadLocalRandomHolder = ThreadLocal.withInitial(Random::new);

    @Benchmark
    public int random() {
        return random.nextInt();
    }

    @Benchmark
    public int threadLocalRandom() {
        return ThreadLocalRandom.current().nextInt();
    }

    @Benchmark
    public int threadLocalRandomHolder() {
        return threadLocalRandomHolder.get().nextInt();
    }

    @Benchmark
    public int nettyThreadLocalRandom() {
        return io.netty.util.internal.ThreadLocalRandom.current().nextInt();
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(RandomBenchmark.class.getSimpleName())
                .build();

        new Runner(opt).run();
    }

}

Результаты оценки следующие:

Benchmark                                Mode  Cnt     Score     Error  Units
RandomBenchmark.nettyThreadLocalRandom   avgt    3   192.202 ± 295.897  ns/op
RandomBenchmark.random                   avgt    3  3197.620 ± 380.981  ns/op
RandomBenchmark.threadLocalRandom        avgt    3    90.731 ±  39.098  ns/op
RandomBenchmark.threadLocalRandomHolder  avgt    3   229.502 ± 267.144  ns/op

Как видно из рисунка выше, JDK1.7ThreadLocalRandomНаилучший результат достигается, для генерации случайного числа требуется всего 90 нс, реализовано nettyThreadLocalRandomИ использование ThreadLocal для поддержки Random не сильно отличается, занимая 2-е и 3-е место, а общая переменная Random — худшее.

Видно, что в параллельных сценариях ThreadLocalRandom может значительно повысить производительность.

5 пунктов на заметку

Обратите внимание: ThreadLocalRandom не забудьте не вызывать текущий метод и использовать его как общую переменную.

public class WrongCase {
    
    ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
    
    public int concurrentNextInt(){
        return threadLocalRandom.nextInt();
    }
    
}

Это связано с тем, что ThreadLocalRandom.current() использует поток, который его инициализировал, для заполнения случайного начального числа, что может привести к тому, что несколько потоков используют одно и то же начальное число.

public class Main {

    public static void main(String[] args) {
        ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
        for(int i=0;i<10;i++)
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(threadLocalRandom.nextInt());
            }
        }).start();

    }
}

Выведите одно и то же случайное число:

-1667209487
-1667209487
-1667209487
-1667209487
-1667209487
-1667209487
-1667209487
-1667209487
-1667209487
-1667209487

Пожалуйста, убедитесь, что разные потоки получают разные начальные значения, самый простой способ — использовать current() для каждого вызова:

public class RightCase {
    public int concurrentNextInt(){
        return ThreadLocalRandom.current().nextInt();
    }
}

Пасхальное яйцо 1

В моей памяти часто всплывает фраза из блога Лян Фэя: дьявол кроется в деталях. Отличный код накапливается один за другим, и представленный сегодня ThreadLocalRandom не является исключением.

dubbo

В incubator-dubbo-2.7.0 небольшое изменение в балансировщике случайной нагрузки заключается в замене Random на ThreadLocalRandom для оптимизации производительности параллелизма.

Пасхальное яйцо 2

В методе nextInt(intbound) ThreadLocalRandom, когда граница не является степенью 2, используется цикл для изменения значения г. Я думаю, что это может не понадобиться, как вы думаете?

public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    int r = mix32(nextSeed());
    int m = bound - 1;
    if ((bound & m) == 0) // power of two
        r &= m;
    else { // reject over-represented candidates
        for (int u = r >>> 1;
             u + m - (r = u % bound) < 0;
             u = mix32(nextSeed()) >>> 1)
            ;
    }
    return r;
}

Добро пожаловать в мою общедоступную учетную запись WeChat: «Обмен технологиями Кирито». На любые вопросы по статье будут даны ответы, что позволит расширить обмен технологиями, связанными с Java.

关注微信公众号