关于java:Java中生成随机数的4种方式

47次阅读

共计 6284 个字符,预计需要花费 16 分钟才能阅读完成。

在 Java 中,生成随机数的场景有很多,所以本文咱们就来盘点一下 4 种生成随机数的形式,以及它们之间的区别和每种生成形式所对应的场景。

1.Random

Random 类诞生于 JDK 1.0,它产生的随机数是伪随机数,也就是有规定的随机数。Random 应用的随机算法为 linear congruential pseudorandom number generator (LGC) 线性同余法伪随机数。在随机数生成时,随机算法的起源数字称为种子数(seed),在种子数的根底上进行肯定的变换,从而产生须要的随机数字。

Random 对象在种子数雷同的状况下,雷同次数生成的随机数是雷同的 。比方两个种子数雷同的 Random 对象,第一次生成的随机数字完全相同,第二次生成的随机数字也完全相同。 默认状况下 new Random() 应用的是以后纳秒工夫作为种子数的

① 根底应用

应用 Random 生成一个从 0 到 10 的随机数(不蕴含 10),实现代码如下:

// 生成 Random 对象
Random random = new Random();
for (int i = 0; i < 10; i++) {
    // 生成 0-9 随机整数
    int number = random.nextInt(10);
    System.out.println("生成随机数:" + number);
}

以上程序的执行后果为:

② 优缺点剖析

Random 应用 LGC 算法生成伪随机数的 长处是执行效率比拟高,生成的速度比拟快

它的 毛病是如果 Random 的随机种子一样的话,每次生成的随机数都是可预测的(都是一样的)。如下代码所示,当咱们给两个线程设置雷同的种子数的时候,会发现每次产生的随机数也是雷同的:

 // 创立两个线程
for (int i = 0; i < 2; i++) {new Thread(() -> {
        // 创立 Random 对象,设置雷同的种子
        Random random = new Random(1024);
        // 生成 3 次随机数
        for (int j = 0; j < 3; j++) {
            // 生成随机数
            int number = random.nextInt();
            // 打印生成的随机数
            System.out.println(Thread.currentThread().getName() + ":" +
                               number);
            // 休眠 200 ms
            try {Thread.sleep(200);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            System.out.println("---------------------");
        }
    }).start();}

以上程序的执行后果为:

③ 线程平安问题

当咱们要应用一个类时,咱们首先关怀的第一个问题是:它是否为线程平安?对于 Random 来说,Random 是线程平安的

PS:线程平安指的是在多线程的场景下,程序的执行后果和预期的后果统一,就叫线程平安的,否则则为非线程平安的(也叫线程平安问题)。比方有两个线程,第一个线程执行 10 万次 ++ 操作,第二个线程执行 10 万次 — 操作,那么最终的后果应该是没加也没减,如果程序最终的后果和预期不符,则为非线程平安的。

咱们来看 Random 的实现源码:

public Random() {this(seedUniquifier() ^ System.nanoTime());
}

public int nextInt() {return next(32);
}

protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed)); // CAS(Compare and Swap)生成随机数
    return (int)(nextseed >>> (48 - bits));
}

PS:本文所有源码来自于 JDK 1.8.0_211。

从以上源码能够看出,Random 底层应用的是 CAS(Compare and Swap,比拟并替换)来解决线程平安问题的,因而对于绝大数随机数生成的场景,应用 Random 不乏为一种很好的抉择。

PS:Java 并发机制实现原子操作有两种:一种是锁,一种是 CAS。

CAS 是 Compare And Swap(比拟并替换)的缩写,java.util.concurrent.atomic 中的很多类,如(AtomicInteger AtomicBoolean AtomicLong 等)都应用了 CAS 机制来实现。

2.ThreadLocalRandom

ThreadLocalRandom 是 JDK 1.7 新提供的类,它属于 JUC(java.util.concurrent)下的一员,为什么有了 Random 之后还会再创立一个 ThreadLocalRandom?

起因很简略,通过下面 Random 的源码咱们能够看出,Random 在生成随机数时应用的 CAS 来解决线程平安问题的,然而 CAS 在线程竞争比拟强烈的场景中效率是非常低的,起因是 CAS 比照时老有其余的线程在批改原来的值,所以导致 CAS 比照失败,所以它要始终循环来尝试进行 CAS 操作。所以 在多线程竞争比拟强烈的场景能够应用 ThreadLocalRandom 来解决 Random 执行效率比拟低的问题

当咱们第一眼看到 ThreadLocalRandom 的时候,肯定会联想到一次类 ThreadLocal,的确如此。ThreadLocalRandom 的实现原理与 ThreadLocal 相似,它相当于给每个线程一个本人的本地种子,从而就能够防止因多个线程竞争一个种子,而带来的额定性能开销了

① 根底应用

接下来咱们应用 ThreadLocalRandom 来生成一个 0 到 10 的随机数(不蕴含 10),实现代码如下:

// 失去 ThreadLocalRandom 对象
ThreadLocalRandom random = ThreadLocalRandom.current();
for (int i = 0; i < 10; i++) {
    // 生成 0-9 随机整数
    int number = random.nextInt(10);
    // 打印后果
    System.out.println("生成随机数:" + number);
}

以上程序的执行后果为:

② 实现原理

ThreadLocalRandom 的实现原理和 ThreadLocal 相似,它是让每个线程持有本人的本地种子,该种子在生成随机数时候才会被初始化,实现源码如下:

public int nextInt(int bound) {
    // 参数效验
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    // 依据以后线程中种子计算新种子
    int r = mix32(nextSeed());
    int m = bound - 1;
    // 依据新种子和 bound 计算随机数
    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;
}

final long nextSeed() {
    Thread t; long r; // read and update per-thread seed
    // 获取以后线程中 threadLocalRandomSeed 变量,而后在种子的根底上累加 GAMMA 值作为新种子
    // 再应用 UNSAFE.putLong 将新种子寄存到以后线程的 threadLocalRandomSeed 变量中
    UNSAFE.putLong(t = Thread.currentThread(), SEED,
                   r = UNSAFE.getLong(t, SEED) + GAMMA); 
    return r;
}

③ 优缺点剖析

ThreadLocalRandom 联合了 Random 和 ThreadLocal 类,并被隔离在以后线程中。因而它通过防止竞争操作种子数,从而 在多线程运行的环境中实现了更好的性能 ,而且也保障了它的 线程平安

另外,不同于 Random,ThreadLocalRandom 明确不反对设置随机种子。它重写了 Random 的
setSeed(long seed) 办法并间接抛出了 UnsupportedOperationException 异样,因而 升高了多个线程呈现随机数反复的可能性

源码如下:

public void setSeed(long seed) {// only allow call from super() constructor
    if (initialized)
        throw new UnsupportedOperationException();}

只有程序中调用了 setSeed() 办法就会抛出 UnsupportedOperationException 异样,如下图所示:

ThreadLocalRandom 毛病剖析

尽管 ThreadLocalRandom 不反对手动设置随机种子的办法,但并不代表 ThreadLocalRandom 就是完满的,当咱们查看 ThreadLocalRandom 初始化随机种子的办法 initialSeed() 源码时发现,默认状况下它的随机种子也是以以后工夫无关,源码如下:

private static long initialSeed() {
    // 尝试获取 JVM 的启动参数
    String sec = VM.getSavedProperty("java.util.secureRandomSeed");
    // 如果启动参数设置的值为 true,则参数一个随机 8 位的种子
    if (Boolean.parseBoolean(sec)) {byte[] seedBytes = java.security.SecureRandom.getSeed(8);
        long s = (long)(seedBytes[0]) & 0xffL;
        for (int i = 1; i < 8; ++i)
            s = (s << 8) | ((long)(seedBytes[i]) & 0xffL);
        return s;
    }
    // 如果没有设置启动参数,则应用以后工夫无关的随机种子算法
    return (mix64(System.currentTimeMillis()) ^
            mix64(System.nanoTime()));
}

从上述源码能够看出,当咱们设置了启动参数“-Djava.util.secureRandomSeed=true”时,ThreadLocalRandom 会产生一个随机种子,肯定水平上能缓解随机种子雷同所带来随机数可预测的问题,然而 默认状况下如果不设置此参数,那么在多线程中就能够因为启动工夫雷同,而导致多个线程在每一步操作中都会生成雷同的随机数

3.SecureRandom

SecureRandom 继承自 Random,该类提供加密强随机数生成器。SecureRandom 不同于 Random,它收集了一些随机事件,比方鼠标点击,键盘点击等,SecureRandom 应用这些随机事件作为种子。这意味着,种子是不可预测的,而不像 Random 默认应用零碎以后工夫的毫秒数作为种子,从而防止了生成雷同随机数的可能性。

根底应用

// 创立 SecureRandom 对象,并设置加密算法
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
for (int i = 0; i < 10; i++) {
    // 生成 0-9 随机整数
    int number = random.nextInt(10);
    // 打印后果
    System.out.println("生成随机数:" + number);
}

以上程序的执行后果为:

SecureRandom 默认反对两种加密算法:

  1. SHA1PRNG 算法,提供者 sun.security.provider.SecureRandom;
  2. NativePRNG 算法,提供者 sun.security.provider.NativePRNG。

当然除了上述的操作形式之外,你还能够抉择应用 new SecureRandom() 来创立 SecureRandom 对象,实现代码如下:

SecureRandom secureRandom = new SecureRandom();

通过 new 初始化 SecureRandom,默认会应用 NativePRNG 算法来生成随机数,然而也能够配置 JVM 启动参数“-Djava.security”参数来批改生成随机数的算法,或抉择应用 getInstance("算法名称") 的形式来指定生成随机数的算法。

4.Math

Math 类诞生于 JDK 1.0,它外面蕴含了用于执行根本数学运算的属性和办法,如初等指数、对数、平方根和三角函数,当然它外面也蕴含了生成随机数的静态方法 Math.random()此办法会产生一个 0 到 1 的 double 值,如下代码所示。

① 根底应用

for (int i = 0; i < 10; i++) {
    // 产生随机数
    double number = Math.random();
    System.out.println("生成随机数:" + number);
}

以上程序的执行后果为:

② 扩大

当然如果你想 用它来生成一个肯定范畴的 int 值 也是能够的,你能够这样写:

for (int i = 0; i < 10; i++) {
    // 生成一个从 0-99 的整数
    int number = (int) (Math.random() * 100);
    System.out.println("生成随机数:" + number);
}

以上程序的执行后果为:

③ 实现原理

通过剖析 Math 的源码咱们能够得悉:当第一次调用 Math.random() 办法时,主动创立了一个伪随机数生成器,实际上用的是 new java.util.Random(),当下一次持续调用 Math.random() 办法时,就会应用这个新的伪随机数生成器。

源码如下:

public static double random() {return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}

private static final class RandomNumberGeneratorHolder {static final Random randomNumberGenerator = new Random();
}

总结

本文咱们介绍了 4 种生成随机数的办法,其中 Math 是对 Random 的封装,所以二者比拟相似。Random 生成的是伪随机数,是以以后纳秒工夫作为种子数的,并且在多线程竞争比拟强烈的状况下因为要进行 CAS 操作,所以存在肯定的性能问题,但 对于绝大数利用场景来说,应用 Random 曾经足够了。当在竞争比拟强烈的场景下能够应用 ThreadLocalRandom 来代替 Random,但如果对安全性要求比拟高的状况下,能够应用 SecureRandom 来生成随机数,因为 SecureRandom 会收集一些随机事件来作为随机种子,所以 SecureRandom 能够看作是生成真正随机数的一个工具类。

参考 & 鸣谢

www.cnblogs.com/weink1215/p/4433790.html

blog.csdn.net/lycyingO/article/details/95276195

关注公号「Java 中文社群」查看更多有意思、涨常识的并发编程文章。

正文完
 0