Можно сказать, что Random — это класс, который знает и много использует каждый разработчик. см. мой блог больше. Но не все знают принцип Random, и должно быть меньше людей, которые знают о недостатках Random при высоком параллелизме. В этом блоге я проанализирую дефекты класса Random при параллелизме и его оптимизацию JUC.
Принцип и недостатки Random
public static void main(String[] args) {
Random random = new Random();
System.out.println(random.nextInt(100));
}
Когда я учился программированию, меня всегда недоумевали разработчики JDK: почему название метода для генерации случайных чисел: ""nextXXX"? Хотя мой английский остается только "nod yes, nod no, come is come, go is go ", но я знаю, что next означает "следующий". Если я назову его, он будет называться "создать", "сгенерировать", разве это не более "подходящее"? Почему разработчики JDK называют его "nextXXX"? может быть, они внезапно «не в словах» и не могут придумать ни одного слова, поэтому они просто придумали одно? Позже я понял, что случайное число, сгенерированное Random, на самом деле не случайное. начальное значение, которое соответствует начальному значению для расчета [следующего] значения, если начальное значение одинаково, то генерируемые им случайные числа также должны быть равными, то есть «определенный ввод производит определенный вывод».
Если вы мне не верите, давайте проведем эксперимент:
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Random random = new Random(15);
System.out.println(random.nextInt(100));
}
}
В дополнение к предоставлению метода построения без параметров, класс Random также предоставляет параметризованный метод построения.Мы можем передать параметр типа int.Этот параметр называется "начальным числом", так что "начальное число" является фиксированным, а сгенерированный случайный Цифры те же:
41
41
41
41
41
41
41
41
41
41
Давайте кратко рассмотрим исходный код nextInt. Исходный код включает в себя алгоритм. Конечно, алгоритм не находится в центре внимания этого обсуждения в блоге. Мы можем упростить исходный код следующим образом:
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
//1.根据老的种子生成新的种子
int r = next(31);
//2.根据新的种子计算随机数
...
return r;
}
Первый заключается в создании нового начального числа на основе старого начального числа, а затем на основе нового начального числа вычисляется случайное число.Основной код nextXXX можно упростить за эти два шага. Теперь давайте подумаем над вопросом: если есть N потоков в случае высокого параллелизма и одновременно выполнить первый шаг: сгенерировать новое начальное число на основе старого начального значения, не будет ли полученное начальное число таким же? Поскольку вторым шагом является вычисление случайных чисел на основе нового начального числа, а этот алгоритм фиксирован, что происходит? Разве случайное число, в конечном итоге, не получается N нитями все равно? Очевидно, это не то, что мы хотим, поэтому об этом подумали разработчики JDK, давайте посмотрим, что происходит внутри следующего метода:
protected int next(int bits) {
long oldseed, nextseed;//定义旧种子,下一个种子
AtomicLong seed = this.seed;
do {
oldseed = seed.get();//获得旧的种子值,赋值给oldseed
nextseed = (oldseed * multiplier + addend) & mask;//一个神秘的算法
} while (!seed.compareAndSet(oldseed, nextseed));//CAS,如果seed的值还是为oldseed,就用nextseed替换掉,并且返回true,退出while循环,如果已经不为oldseed了,就返回false,继续循环
return (int)(nextseed >>> (48 - bits));//一个神秘的算法
}
- Определяет старое семя oldseed, следующее семя (новое семя) nextseed.
- Получите значение старого семени и назначьте его oldseed.
- Загадочный алгоритм, который вычисляет следующее начальное число (новое начальное число) и присваивает его nextseed.
- Используя операцию CAS, если значение seed все еще oldseed, замените его на nextseed и верните true, ! Если true равно false, выходим из цикла while; если значение seed больше не oldseed, это означает, что значение seed заменено, возвращаем false! false равно true, переходите к следующему циклу while.
- Таинственный алгоритм, вычисляющий случайное число на основе nextseed и возвращающий его.
Мы видим, что ядро находится на четвертом шаге, опишу его подробнее, во-первых, мы должны знать тип seed:
private final AtomicLong seed;
Тип начального числа — AtomicLong, который является атомарным операционным классом, который может гарантировать атомарность. Переменная. Когда два потока одновременно входят в следующий метод, происходит следующее:
- Поток A и поток B одновременно получают значение seed и присваивают его переменной oldseed.
- Согласно таинственному алгоритму, nextseed вычисляется как XXX. Обратите внимание, что, поскольку этот алгоритм фиксирован, сгенерированное nextseed также должно быть исправлено.
- Введите цикл while: 3.1 Поток A, используя алгоритм CAS, обнаруживает, что значение seed все еще является oldseed, указывая на то, что значение seed не было заменено, и заменяет значение seed на nextseed, сгенерированное на втором шаге, замена прошла успешно, и возвращает истину! true в false, выйти из цикла while. 3.2 Поток B, используя алгоритм CAS, обнаруживает, что значение seed больше не является oldseed, потому что поток A заменил значение seed на nextseed, поэтому CAS терпит неудачу и может только снова зацикливаться. При повторном цикле seed.get() получает последнее начальное значение и снова получает nextSeed в соответствии с загадочным алгоритмом, CAS завершается успешно и выходит из цикла.
Все выглядит хорошо, но это не так Что произойдет, если параллелизм будет высоким? Большое количество потоков находится в цикле while, что довольно интенсивно использует ЦП, поэтому JUC представила ThreadLocalRandom для решения этой проблемы.
ThreadLocalRandom
Во-первых, давайте посмотрим, как используется ThreadLocalRandom:
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
System.out.println(threadLocalRandom.nextInt(100));
}
}
Вы можете видеть, что использование немного изменилось.Давайте посмотрим на логику реализации основного кода ThreadLocalRandom:
current
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
Следует отметить, что, поскольку current является статическим методом, если этот метод вызывается несколько раз, возвращаемый объект ThreadLocalRandom будет одним и тем же.
Если PROBE текущего потока равен 0, это означает, что текущий метод вызывается в первый раз, тогда необходимо вызвать метод localInit, в противном случае сгенерированный экземпляр возвращается напрямую.
localInit
static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p; // skip 0
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
UNSAFE.putLong(t, SEED, seed);
UNSAFE.putInt(t, PROBE, probe);
}
Сначала инициализируйте зонд и начальное значение, а затем вызовите метод класса UNSAFE, чтобы установить зонд и начальное значение для текущего потока, чтобы другие потоки не могли его получить.
nextInt
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;
}
Так же, как метод nextXXX в классе Random, он также генерирует новое начальное число на основе старого начального числа, а затем генерирует случайное число на основе нового начального числа Давайте посмотрим, что делает метод nextSeed:
nextSeed
final long nextSeed() {
Thread t; long r; // read and update per-thread seed
UNSAFE.putLong(t = Thread.currentThread(), SEED,
r = UNSAFE.getLong(t, SEED) + GAMMA);
return r;
}
Сначала используйте UNSAFE.getLong(t, SEED), чтобы получить SEED текущего потока, затем + GAMMA в качестве нового начального значения, а затем поместите новое начальное значение в текущий поток.
Суммировать
В этой статье сначала кратко анализируется принцип реализации Random и выявляется недостаток метода nextXXX при высокой степени параллелизма: ему необходимо конкурировать за атомарную переменную seed. Затем вводится использование и принцип ThreadLocalRandom. Из названия класса видно, что принцип реализации аналогичен ThreadLocal. Начальное число сохраняется в каждом потоке, а новое число вычисляется в соответствии с исходным значением в каждый поток. , тем самым избегая проблемы конкуренции.