ThreadLocalRandom: примеры (JAVA)

Класс ThreadLocalRandom: эффективный генератор случайных чисел для многопоточных приложений
Раздел: Генерация случайных чисел
ThreadLocalRandom

Описание класса ThreadLocalRandom

ThreadLocalRandom - это класс из пакета java.util.concurrent, предназначенный для генерации случайных чисел в многопоточных средах. В отличие от java.util.Random, который при совместном использовании несколькими потоками может приводить к конкуренции (contention) и снижению производительности, ThreadLocalRandom использует отдельный экземпляр генератора для каждого потока. Таким образом, вызовы методов не синхронизируются и не блокируют другие потоки.

Когда используется: класс применяется в высоконагруженных параллельных вычислениях, в параллельных стримах, в задачах с большим количеством потоков, где требуется большое количество случайных чисел. Он предпочтительнее Random и Math.random() при работе с многопоточностью.

Аргументы методов: Основные методы не принимают аргументов (например, nextInt()) или принимают один целочисленный аргумент - верхнюю границу (исключительно) для равномерного распределения. Также есть перегруженные версии с двумя аргументами: origin (включительно) и bound (исключительно) для генерации чисел в произвольном диапазоне. Для вещественных чисел nextDouble(), nextFloat() могут принимать один или два аргумента. nextBoolean() аргументов не имеет. nextLong() работает аналогично nextInt(). nextGaussian() возвращает нормально распределённое значение с математическим ожиданием 0 и стандартным отклонением 1. nextBytes(byte[]) заполняет переданный массив случайными байтами.

Возвращаемые значения: Все методы возвращают примитивные типы: int, long, double, float, boolean, byte[] (заполняется), double для гауссова распределения.

Для получения экземпляра используется статический метод ThreadLocalRandom.current(). Экземпляр не нужно хранить долго - он автоматически связывается с текущим потоком.

Примеры использования

import java.util.concurrent.ThreadLocalRandom;

public class Example {
    public static void main(String[] args) {
        // Случайное целое число
        int randomInt = ThreadLocalRandom.current().nextInt();
        System.out.println("nextInt(): " + randomInt);

        // Целое от 0 до 9 (исключительно)
        int bounded = ThreadLocalRandom.current().nextInt(10);
        System.out.println("nextInt(10): " + bounded);

        // Целое от 5 до 15 (5 включительно, 15 исключительно)
        int ranged = ThreadLocalRandom.current().nextInt(5, 15);
        System.out.println("nextInt(5,15): " + ranged);

        // Случайное double [0,1)
        double randomDouble = ThreadLocalRandom.current().nextDouble();
        System.out.println("nextDouble(): " + randomDouble);

        // Double от 0 до 5 (исключительно)
        double boundedDouble = ThreadLocalRandom.current().nextDouble(5.0);
        System.out.println("nextDouble(5.0): " + boundedDouble);

        // Double от 2.5 до 7.3
        double rangedDouble = ThreadLocalRandom.current().nextDouble(2.5, 7.3);
        System.out.println("nextDouble(2.5,7.3): " + rangedDouble);

        // Случайный boolean
        boolean bool = ThreadLocalRandom.current().nextBoolean();
        System.out.println("nextBoolean(): " + bool);

        // Гауссово распределение (нормальное)
        double gaussian = ThreadLocalRandom.current().nextGaussian();
        System.out.println("nextGaussian(): " + gaussian);

        // Заполнение массива байтов
        byte[] bytes = new byte[6];
        ThreadLocalRandom.current().nextBytes(bytes);
        System.out.print("nextBytes(): ");
        for (byte b : bytes) System.out.print(b + " ");
        System.out.println();
    }
}
nextInt(): -1234567890
nextInt(10): 7
nextInt(5,15): 11
nextDouble(): 0.6738294567839123
nextDouble(5.0): 3.294851234678912
nextDouble(2.5,7.3): 5.912345678901234
nextBoolean(): true
nextGaussian(): -0.3528765432109876
nextBytes(): 45 -123 78 0 12 -56

Примечание: фактические значения при каждом запуске будут разными.

Альтернативные классы для генерации случайных чисел в Java

  • java.util.Random - базовый класс. При использовании из нескольких потоков требуется синхронизация, что снижает производительность. Подходит для однопоточных сценариев или низкой нагрузки.

  • java.security.SecureRandom - криптостойкий генератор. Используется для задач безопасности (генерация ключей, токенов). Работает медленнее, чем ThreadLocalRandom, но выдаёт непредсказуемые последовательности.

  • java.util.SplittableRandom - ещё один генератор, предназначенный для параллельных вычислений. Он позволяет создавать «расщеплённые» экземпляры, независимые друг от друга. Хорошо подходит для параллельных стримов и комбинированных задач, где нужно раздавать генераторы ветвям вычислений.

  • Math.random() - удобный, но синхронизированный вызов. Внутри использует один экземпляр Random. Не подходит для многопоточных высоконагруженных систем.

Выбор зависит от контекста: для многопоточных сценариев с высокой нагрузкой ThreadLocalRandom - оптимальный вариант; для криптографии - SecureRandom; для параллельных алгоритмов с «ветвлением» - SplittableRandom.

Аналоги ThreadLocalRandom в других языках программирования

  • PHP: функции rand(), mt_rand() не являются потокобезопасными. Для многопоточных сред (pthreads) используются отдельные экземпляры генератора. random_int() криптостойкий.

    // PHP – каждый поток создаёт свой экземпляр
    $random = new Random\Randomizer();
    echo $random->getInt(0, 10); // 7
  • JavaScript: Math.random() - единственный стандартный генератор. Он не является потокобезопасным в среде Node.js (используется один поток). Для многопоточности (Worker) каждый воркер вызывает свой Math.random().

    // JavaScript (Node.js)
    const r = Math.floor(Math.random() * 10); // 3
  • Python: модуль random - использует глобальное состояние (Mersenne Twister). Для потокобезопасности нужно создавать отдельные объекты Random в каждом потоке. secrets для криптостойкости.

    import random
    r = random.Random()
    print(r.randint(0, 10)) # 7
  • SQL: RANDOM() (PostgreSQL) или NEWID() (SQL Server) не привязаны к потокам. Каждая запись получает своё случайное число.

    SELECT floor(random() * 10)::int; -- PostgreSQL
  • C#: класс System.Random не является потокобезопасным. Рекомендуется использовать ThreadLocal<Random> или Random.Shared (начиная с .NET 6).

    // C# .NET 6+
    int r = Random.Shared.Next(10); // 4
  • Lua: math.random() - глобальное состояние. Для многопоточности (Lua-lanes) нужно изолировать состояния.

    -- Lua
    math.randomseed(os.time())
    print(math.random(1,10)) -- 8
  • Go: пакет math/rand - глобальный генератор с мьютексом (потокобезопасен, но медленнее). rand.NewSource создаёт независимые источники. Для потокобезопасности без блокировок - rand.New(&lockedSource).

    // Go
    import "math/rand"
    r := rand.New(rand.NewSource(42))
    println(r.Intn(10)) // 2
  • Kotlin: использует Java-классы. ThreadLocalRandom.current() доступен напрямую. Также есть kotlin.random.Random, который под капотом использует ThreadLocalRandom.

    // Kotlin
    import kotlin.random.Random
    println(Random.nextInt(10)) // 7

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

Типичные ошибки при использовании ThreadLocalRandom

  • Забыли вызвать current(): ThreadLocalRandom - статический класс, экземпляр получается через ThreadLocalRandom.current() каждый раз при вызове. Если сохранить ссылку и использовать её в другом потоке, она будет работать неправильно (всё равно будет связана с исходным потоком).

    // Неправильно:
    ThreadLocalRandom rnd = ThreadLocalRandom.current();
    new Thread(() -> System.out.println(rnd.nextInt())).start(); // rnd всё ещё от родительского потока!
    Ошибка: генератор может отработать, но задумка нарушена; лучше вызывать current() в дочернем потоке.
  • Путаница с границами: в методах вида nextInt(origin, bound) нижняя граница включительна, верхняя - исключительна. Ошибка - думать, что обе включительны.

    int val = ThreadLocalRandom.current().nextInt(10, 20); // 10..19, не 20
    System.out.println(val); // может быть 19, но не 20
  • Невозможность установки seed: ThreadLocalRandom не позволяет задавать начальное значение (seed) для воспроизводимости. Если нужна детерминированная последовательность, используйте Random с известным seed.

  • Использование в однопоточных сценариях: в однопоточном коде ThreadLocalRandom работает, но Random может быть быстрее из-за отсутствия oveголовов на извлечение из ThreadLocal. Однако разница незначительна.

  • Передача экземпляра между потоками: сохранение ThreadLocalRandom в поле объекта и использование из разных потоков - ошибка. Всегда получайте текущий экземпляр внутри потока.

Изменения в ThreadLocalRandom в последних версиях Java

  • Java 7: класс был введён в составе пакета java.util.concurrent. Базовая функциональность: current(), nextInt(), nextDouble(), nextLong(), nextBoolean(), nextFloat(), nextGaussian().

  • Java 8: добавлены методы ints(), longs(), doubles(), возвращающие неограниченные или ограниченные потоки (Stream) случайных чисел. Это позволило интегрировать генерацию с параллельными стримами без явного вызова current() в каждом элементе.

  • Java 17 и 21: существенных изменений не произошло. Класс остаётся стабильным. Оптимизирована внутренняя реализация (например, использование VarHandle вместо Unsafe в более ранних версиях).

  • Будущие версии: ожидается, что функциональность останется неизменной. Разработчикам рекомендуется использовать ThreadLocalRandom для новых многопоточных проектов, где не требуется криптостойкость.

Расширенные и нестандартные примеры использования

1. Использование в параллельных стримах (Parallel Streams)

Пример java
import java.util.stream.IntStream;
import java.util.concurrent.ThreadLocalRandom;

public class ParallelStreamExample {
    public static void main(String[] args) {
        long sum = IntStream.range(0, 1000)
                .parallel()
                .map(i -> ThreadLocalRandom.current().nextInt(100))
                .sum();
        System.out.println("Сумма 1000 случайных чисел: " + sum);
    }
}
Сумма 1000 случайных чисел: 49237

Пояснение: каждый элемент стрима обрабатывается в своём потоке, и вызов ThreadLocalRandom.current() возвращает экземпляр, привязанный к этому потоку. Благодаря этому нет конкуренции.

2. Генерация случайного массива байтов заданной длины

Пример java
import java.util.concurrent.ThreadLocalRandom;

public class RandomBytesArray {
    public static void main(String[] args) {
        byte[] arr = new byte[100];
        ThreadLocalRandom.current().nextBytes(arr);
        // arr теперь заполнен случайными байтами
        System.out.println("Первый байт: " + arr[0]);
    }
}
Первый байт: -82

3. Генерация случайной строки (алфавитно-цифровая)

Пример java
import java.util.concurrent.ThreadLocalRandom;

public class RandomString {
    private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

    public static String generateRandomString(int length) {
        StringBuilder sb = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            int index = ThreadLocalRandom.current().nextInt(ALPHABET.length());
            sb.append(ALPHABET.charAt(index));
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        System.out.println(generateRandomString(10));
    }
}
aB3kLqW9Xz

4. Комбинирование с CompletableFuture (асинхронные задачи)

Пример java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;

public class CompletableFutureExample {
    public static void main(String[] args) throws Exception {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            // Этот код выполняется в потоке ForkJoinPool.commonPool()
            return ThreadLocalRandom.current().nextInt(1, 100);
        });
        System.out.println("Случайное число: " + future.get());
    }
}
Случайное число: 54

5. Использование с ForkJoinPool

Пример java
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ThreadLocalRandom;

class RandomSumTask extends RecursiveTask<Integer> {
    private int threshold;
    RandomSumTask(int threshold) { this.threshold = threshold; }

    @Override
    protected Integer compute() {
        if (threshold <= 1) {
            return ThreadLocalRandom.current().nextInt(10);
        }
        RandomSumTask subTask = new RandomSumTask(threshold / 2);
        subTask.fork();
        int result = ThreadLocalRandom.current().nextInt(10);
        return result + subTask.join();
    }
}

public class ForkJoinExample {
    public static void main(String[] args) {
        ForkJoinPool pool = ForkJoinPool.commonPool();
        int sum = pool.invoke(new RandomSumTask(4));
        System.out.println("Сумма случайных чисел: " + sum);
    }
}
Сумма случайных чисел: 23

6. Использование методов-потоков (ints, longs, doubles) с ограничением

Пример java
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.IntStream;

public class StreamMethodsExample {
    public static void main(String[] args) {
        // Бесконечный поток целых чисел от 0 до 100, взято 10 первых
        IntStream stream = ThreadLocalRandom.current().ints(0, 100);
        stream.limit(10).forEach(System.out::println);

        // Поток из ровно 5 чисел
        ThreadLocalRandom.current().ints(5, 1, 50)
                .forEach(System.out::println);
    }
}
23
87
12
45
9
67
34
11
78
6
--- (второй набор)
7
42
19
33
28

7. Использование в лямбда-выражениях без явного хранения экземпляра

Пример java
import java.util.function.Supplier;
import java.util.concurrent.ThreadLocalRandom;

public class LambdaExample {
    public static void main(String[] args) {
        Supplier<Integer> randomSupplier = () -> ThreadLocalRandom.current().nextInt(1, 100);
        System.out.println(randomSupplier.get());
        System.out.println(randomSupplier.get());
    }
}
34
78

Пояснение: при каждом вызове get() из лямбды вызывается ThreadLocalRandom.current(), что безопасно при параллельном выполнении (если supplier будет передан в parallel stream).

джава ThreadLocalRandom function comments

En
ThreadLocalRandom A random number generator isolated to the current thread