文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Java生成随机数的4种方式,以后就用它了!

2024-12-03 03:22

关注

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

1.Random

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

Random 对象在种子数相同的情况下,相同次数生成的随机数是相同的。比如两个种子数相同的 Random 对象,第一次生成的随机数字完全相同,第二次生成的随机数字也完全相同。默认情况下 new Random() 使用的是当前纳秒时间作为种子数的。

① 基础使用

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

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

以上程序的执行结果为:

② 优缺点分析

Random 使用 LGC 算法生成伪随机数的优点是执行效率比较高,生成的速度比较快。

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

  1. // 创建两个线程 
  2. or (int i = 0; i < 2; i++) { 
  3.    new Thread(() -> { 
  4.        // 创建 Random 对象,设置相同的种子 
  5.        Random random = new Random(1024); 
  6.        // 生成 3 次随机数 
  7.        for (int j = 0; j < 3; j++) { 
  8.            // 生成随机数 
  9.            int number = random.nextInt(); 
  10.            // 打印生成的随机数 
  11.            System.out.println(Thread.currentThread().getName() + ":" + 
  12.                               number); 
  13.            // 休眠 200 ms 
  14.            try { 
  15.                Thread.sleep(200); 
  16.            } catch (InterruptedException e) { 
  17.                e.printStackTrace(); 
  18.            } 
  19.            System.out.println("---------------------"); 
  20.        } 
  21.    }).start(); 

以上程序的执行结果为:

③ 线程安全问题

当我们要使用一个类时,我们首先关心的第一个问题是:它是否为线程安全?对于 Random 来说,Random 是线程安全的。

我们来看 Random 的实现源码:

  1. public Random() { 
  2.     this(seedUniquifier() ^ System.nanoTime()); 
  3.  
  4. public int nextInt() { 
  5.     return next(32); 
  6.  
  7. protected int next(int bits) { 
  8.     long oldseed, nextseed; 
  9.     AtomicLong seed = this.seed; 
  10.     do { 
  11.         oldseed = seed.get(); 
  12.         nextseed = (oldseed * multiplier + addend) & mask; 
  13.     } while (!seed.compareAndSet(oldseed, nextseed)); // CAS(Compare and Swap)生成随机数 
  14.     return (int)(nextseed >>> (48 - bits)); 

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

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),实现代码如下:

  1. // 得到 ThreadLocalRandom 对象 
  2. ThreadLocalRandom random = ThreadLocalRandom.current(); 
  3. for (int i = 0; i < 10; i++) { 
  4.     // 生成 0-9 随机整数 
  5.     int number = random.nextInt(10); 
  6.     // 打印结果 
  7.     System.out.println("生成随机数:" + number); 

以上程序的执行结果为:

② 实现原理

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

  1. public int nextInt(int bound) { 
  2.     // 参数效验 
  3.     if (bound <= 0) 
  4.         throw new IllegalArgumentException(BadBound); 
  5.     // 根据当前线程中种子计算新种子 
  6.     int r = mix32(nextSeed()); 
  7.     int m = bound - 1; 
  8.     // 根据新种子和 bound 计算随机数 
  9.     if ((bound & m) == 0) // power of two 
  10.         r &= m; 
  11.     else { // reject over-represented candidates 
  12.         for (int u = r >>> 1; 
  13.              u + m - (r = u % bound) < 0; 
  14.              u = mix32(nextSeed()) >>> 1) 
  15.             ; 
  16.     } 
  17.     return r; 
  18.  
  19. final long nextSeed() { 
  20.     Thread t; long r; // read and update per-thread seed 
  21.     // 获取当前线程中 threadLocalRandomSeed 变量,然后在种子的基础上累加 GAMMA 值作为新种子 
  22.     // 再使用 UNSAFE.putLong 将新种子存放到当前线程的 threadLocalRandomSeed 变量中 
  23.     UNSAFE.putLong(t = Thread.currentThread(), SEED, 
  24.                    r = UNSAFE.getLong(t, SEED) + GAMMA);  
  25.     return r; 

③ 优缺点分析

ThreadLocalRandom 结合了 Random 和 ThreadLocal 类,并被隔离在当前线程中。因此它通过避免竞争操作种子数,从而在多线程运行的环境中实现了更好的性能,而且也保证了它的线程安全。

另外,不同于 Random, ThreadLocalRandom 明确不支持设置随机种子。它重写了 Random 的setSeed(long seed) 方法并直接抛出了 UnsupportedOperationException异常,因此降低了多个线程出现随机数重复的可能性。

源码如下:

  1. public void setSeed(long seed) { 
  2.     // only allow call from super() constructor 
  3.     if (initialized) 
  4.         throw new UnsupportedOperationException(); 

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

ThreadLocalRandom 缺点分析

虽然 ThreadLocalRandom 不支持手动设置随机种子的方法,但并不代表 ThreadLocalRandom 就是完美的,当我们查看 ThreadLocalRandom 初始化随机种子的方法 initialSeed() 源码时发现,默认情况下它的随机种子也是以当前时间有关,源码如下:

  1. private static long initialSeed() { 
  2.     // 尝试获取 JVM 的启动参数 
  3.     String sec = VM.getSavedProperty("java.util.secureRandomSeed"); 
  4.     // 如果启动参数设置的值为 true,则参数一个随机 8 位的种子 
  5.     if (Boolean.parseBoolean(sec)) { 
  6.         byte[] seedBytes = java.security.SecureRandom.getSeed(8); 
  7.         long s = (long)(seedBytes[0]) & 0xffL; 
  8.         for (int i = 1; i < 8; ++i) 
  9.             s = (s << 8) | ((long)(seedBytes[i]) & 0xffL); 
  10.         return s; 
  11.     } 
  12.     // 如果没有设置启动参数,则使用当前时间有关的随机种子算法 
  13.     return (mix64(System.currentTimeMillis()) ^ 
  14.             mix64(System.nanoTime())); 

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

3.SecureRandom

SecureRandom 继承自 Random,该类提供加密强随机数生成器。SecureRandom 不同于 Random,它收集了一些随机事件,比如鼠标点击,键盘点击等,SecureRandom 使用这些随机事件作为种子。这意味着,种子是不可预测的,而不像 Random 默认使用系统当前时间的毫秒数作为种子,从而避免了生成相同随机数的可能性。

基础使用

  1. // 创建 SecureRandom 对象,并设置加密算法 
  2. SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); 
  3. for (int i = 0; i < 10; i++) { 
  4.     // 生成 0-9 随机整数 
  5.     int number = random.nextInt(10); 
  6.     // 打印结果 
  7.     System.out.println("生成随机数:" + number); 

以上程序的执行结果为:

SecureRandom 默认支持两种加密算法:

当然除了上述的操作方式之外,你还可以选择使用 new SecureRandom() 来创建 SecureRandom 对象,实现代码如下:

  1. SecureRandom secureRandom = new SecureRandom(); 

通过 new 初始化 SecureRandom,默认会使用 NativePRNG 算法来生成随机数,但是也可以配置 JVM 启动参数“-Djava.security”参数来修改生成随机数的算法,或选择使用 getInstance("算法名称") 的方式来指定生成随机数的算法。

4.Math

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

① 基础使用

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

以上程序的执行结果为:

② 扩展

当然如果你想用它来生成一个一定范围的 int 值也是可以的,你可以这样写:

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

以上程序的执行结果为:

③ 实现原理

通过分析 Math 的源码我们可以得知:当第一次调用 Math.random() 方法时,自动创建了一个伪随机数生成器,实际上用的是 new java.util.Random(),当下一次继续调用 Math.random() 方法时,就会使用这个新的伪随机数生成器。

源码如下:

  1. public static double random() { 
  2.     return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble(); 
  3.  
  4. private static final class RandomNumberGeneratorHolder { 
  5.     static final Random randomNumberGenerator = new Random(); 

 总结

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

 

来源:Java中文社群内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯