【Java】深入理解Java随机数

Java 专栏收录该内容
91 篇文章 1 订阅

随机数

随机数是计算机科学中很重要的内容,实际开发中我们可以用它来生成验证码或者对重复性要求不高的id,也可以用它抽奖。

随机数很重要的,而使用不当就不太友好了,因此有必要研究一下呢。

Math.random()对初学者友好

定义:public static double random()

源码实现:

public static double random() {
    return Math.RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
private static final class RandomNumberGeneratorHolder {
    static final Random randomNumberGenerator = new Random();

    private RandomNumberGeneratorHolder() {
    }
}

根据源码分析得:
其实 java.lang.Math 类里有一个私有静态内部类,内有一个静态的 java.util.Random 类对象,调用其 nextDouble() 方法,生成 [0.0, 1.0) 范围内的伪随机浮点数。

所以其实大可不必用 Math.random() 。

注意:
使用的时候别忘了强转int或者long。
如下:Math.random()*1000000000,随机生成的一个例子:1.368630414562525E8
可见不强转是不行的,除非我们需要的是浮点数。

java.util.Random真香

先熟悉一些生成随机数的API

protected int next​(int bits):生成下一个伪随机数。(注意protected,直接调用不了的)
public boolean nextBoolean():从此随机数生成器的序列中返回下一个伪随机、均匀分布的布尔值。
public void nextBytes​(byte[] bytes):生成随机字节并将其放入用户提供的字节数组中。
public double nextDouble():返回下一个伪随机数,它是此随机数生成器的序列中介于0.0和1.0之间的均匀分布的double值。
public float nextFloat():返回下一个伪随机数,此随机数生成器的序列在0.0和1.0之间均匀分布的float值。
public double nextGaussian():返回下一个伪随机数,与该随机数生成器的序列的 μ=0.0,σ2=1.0 的高斯(“正态”)分布双精度值。
public int nextInt():返回下一个伪随机数,它是此随机数生成器序列中均匀分布的int值。
public int nextInt​(int bound):返回一个伪随机数,它从此随机数生成器的序列中提取,在0()和指定值(不含)之间均匀分布的int值。
public long nextLong():返回下一个伪随机数,该随机数是从此随机数生成器的序列中均匀分布的long值。

不懂均匀分布、正态分布、μ、σ2 的读者请复习概率论,此处并不想讲。

生成随机整数的例子

我这里就且只生成整数吧。
nextInt()nextInt(int bound) 为例,如果用前者,就没限制边界,不符合我们的需求。
后者的使用也必须从左边0端开始,所以需要调整。

比如new Random().nextInt(1000000000),就生成 [0, 1000000000) 内的随机int值。

我们如果要生成 [100000000, 1000000000) 的话,就需要降低右边界的数值并整体加左边界数值,比如:
new Random().nextInt(900000000)+100000000

为此,我自己封装了一个函数:

private static int mathRandom(int from, int to) {
    return new Random().nextInt(to-from)+from;
}

如此,输出两端的边界,就可以生成左闭右开区间的随机整数了呢。
(关于左闭右开的理由分析请看此处 → Here

逐位生成的大随机数

有的人可能会说,我们逐位生成,再用字符串拼接起来就可以啦,就像下面这样:

Random random = new Random();
StringBuilder result = new StringBuilder();
for (int i = 0; i < 9; i++) {
    result.append(random.nextInt(10));
}
System.out.println(result);

但我们可能得到这种答案:
055609793

开头带0的肯定不对啊,只有这样做才能得到答案:

System.out.println(Long.parseLong(result.toString()));

55609793

但这样是对的吗?当然不,因为小于了下限100000000。

有的人可能会说:你高位生成[1, 10),余下9个低位生成[0, 10)不就好了?
麻不麻烦?为什么不一步完成呢?
再说,我反复运行随机数程序N次,电脑风扇就会响得厉害,宁懂看似简单的随机数背后有多少运算嘛……

nextInt​(int bound)与nextInt()の比较

先看源码:

public int nextInt() {
    return this.next(32);
}
public int nextInt(int bound) {
    if (bound <= 0) {
        throw new IllegalArgumentException("bound must be positive");
    } else {
        int r = this.next(31);
        int m = bound - 1;
        if ((bound & m) == 0) {
            r = (int)((long)bound * (long)r >> 31);
        } else {
            for(int u = r; u - (r = u % bound) + m < 0; u = this.next(31)) {
            }
        }

        return r;
    }
}

nextInt​(int bound) 比 nextInt() 多做了几件事。

首先获取一个 31 位的随机数,注意这里是 31 位,和nextInt()的 32 位不同。
因为在 nextInt() 方法中可以获取到随机数可能是负数,即使不读源码,这也是我们测试一下就能知道的。
而 nextInt(int bound) 规定只能获取到 [0,bound) 之前的随机数,也就意味着必须是正数,预留一位符号位,所以只获取了31位。
有人可能会问:为什么不用一下Math.abs()取绝对值呢?
答复是:性能会下降很多,不必。

然后进行取 bound 操作。
如果 bound 是2的幂次方,可以直接将第一步获取的值乘以 bound 然后右移31位。
以 bound==8 为例 ,乘以8其实就是左移3位,其实就是变成了34位,再右移31位的话,就又会变成3位,最后,3位 int 的范围即 [0000, 0111] ,其实就是 [0,8) 了。
如果如果 bound 是2的幂次方,会通过取模运算进行处理。

Random与seed

对于 java.util.Random,JVM 通过传入的种子(seed)来确定生成随机数的区间。

种子是一个数字,可称“种子值”,它为生成新的随机数提供了基础。
只要种子值相同,获取的随机数的序列就是一致的,而且生成的结果都是可以预测的。

这里seed对象的类型是 java.util.concurrent.AtomicLong,而不是简简单单的long。

种子值还是可变的。
public void setSeed​(long seed) 方法可生成随机数:

public synchronized void setSeed(long seed) {
    this.seed.set(initialScramble(seed));
    this.haveNextNextGaussian = false;
}

调用的initialScramble():

private static long initialScramble(long seed) {
    return (seed ^ 25214903917L) & 281474976710655L;
}

这是一种伪随机数的实现,而不是真正的随机数 。
毕竟我们也知道,伪随机只是统计学上的概念,之所以称“伪”,是因为生成的随机数其实是有一定规律的,而这个规律出现的周期随着伪随机算法的优劣而不同。一般来说这个“周期”比较长,但是也是可以预测的。

神秘的next(int bits)与暗藏的CAS

再看看next(int bits)方法:

protected int next(int bits) {
    AtomicLong seed = this.seed;

    long oldseed;
    long nextseed;
    do {
        oldseed = seed.get();
        nextseed = oldseed * 25214903917L + 11L & 281474976710655L;
    } while(!seed.compareAndSet(oldseed, nextseed));

    return (int)(nextseed >>> 48 - bits);
}

Random 类是线程安全的,根据 next(int bits) 还有seed的属性,可以看出其内部使用 CAS 来保证线程安全性。
(不懂CAS自行学习OS或者并发去,这里不讲)

一般而言,CAS 相比加锁有一定的优势(参考“乐观锁”),但并不一定意味着高效
我们可以在每次使用 Random 时都去 new 一个新的线程私有化的 Random 对象。

不过呢,java.util.Random 在多线程并发的情况下的时候是可以优化的。
我们也可以使用 java.lang.ThreadLocal 来维护线程私有化对象。
更是可以使用 java.util.concurrent.ThreadLocalRandom。
后面提到的这两种策略其实都挺好,ThreadLocalRandom会更好也更方便一些。
测试表明:在并发场景下,ThreadLocalRandom 可以明显的提升性能。(感兴趣自己去测一下呗)

再谈并发环境下的Random

java.util.Random的用法是线程安全的。但是,在不同线程上并发使用相同的Random实例可能会导致争用,从而导致性能不佳。
问题源于——使用种子来生成随机数。

(种子在前面提过了,感兴趣自行了解更多……)

首先,旧种子和新种子存储在两个辅助变量上。在这一点上,创造新种子的规则并不重要。
要保存新种子,使用 compareAndSet() 方法将旧种子替换为下一个新种子,但这仅仅在旧种子对应于当前设置的种子的条件下才会触发。
如果此时的值由并发线程操纵,则该方法返回false,这意味着旧值与例外值不匹配。因为是循环内进行的操作,那么会发生自旋,直到变量与例外值匹配。这可能会导致性能不佳和线程竞争。(参考自旋锁的内容)

Random终篇

前面已经说过了,在普通的编程中,Random是很Nice的选择且使用方便,Random甚至能通过CAS机制保证线程安全。
然而,我们也说了,在高并发的情况下,Random可能就不那么好用了,此时,新的篇章缓缓开启——java.util.concurrent.ThreadLocalRandom……

多线程下的ThreadLocalRandom

ThreadLocalRandom 类位于神圣的 java.util.concurrent 包下,继承自Random类,先Orz为敬!!

初识ThreadLocalRandom

ThreadLocalRandom是隔离到当前线程的随机数生成器。
像Math类使用的全局Random生成器一样,ThreadLocalRandom会使用内部生成的种子进行初始化,否则无法进行修改。
ThreadLocalRandom 继承/扩展 了Random并添加选项以限制其使用到相应的线程实例。为此,ThreadLocalRandom的实例保存在相应线程的内部映射中,并通过调用current()来返回对应的Random。
如果适用的话,在并发程序中使用ThreadLocalRandom而不是共享Random对象通常会遇到更少的开销和竞争。
当多个任务(例如,每个ForkJoinTask)在线程池中并行使用随机数时,使用ThreadLocalRandom特别合适。

结合前面说的一些内容,我们不难对ThreadLocalRandom有个初步的了解呢~

ThreadLocalRandom的使用

其构造器是private的:

private ThreadLocalRandom() {}

获取对象要调用current():

public static ThreadLocalRandom current() {
    if (U.getInt(Thread.currentThread(), PROBE) == 0) {
        localInit();
    }

    return instance;
}

当所有用法都是这种形式时,永远不可能在多个线程之间意外地共享ThreadLocalRandom。

使用标程:

ThreadLocalRandom.current().nextX()

nextX()包括nextBoolean()、nextDouble()、nextFloat()、nextInt()、nextLong()等……

例如:

ThreadLocalRandom.current().nextInt()

ThreadLocalRandom与加密安全

ThreadLocalRandom的实例不是加密安全的,还是普通的“伪随机数”。考虑在对安全敏感的应用程序中使用SecureRandom。此外,除非系统属性java.util.secureRandomSeed设置为true,否则默认构造的实例不会使用加密的随机种子。

那么,接着看下去吧~

加密安全的SecureRandom

SecureRandom导语

通过对Random的一些分析我们可以知道Random事实上是伪随机,是可以推导出规律的,而且依赖种子(seed)。如果我们搞抽奖或者其他一些对随机数敏感的场景时,用Random就不合适了,容易被人钻空子。JDK提供了 java.security.SecureRandom 来解决这个事情。

这个Part有点玄学,看不懂就Pass,问题不大……

初识SecureRandom

SecureRandom提供了加密功能强的随机数生成器(RNG)。

加密强度高的随机数至少要符合FIPS 140-2“加密模块的安全性要求”第4.9.1节中指定的统计随机数生成器测试。此外,SecureRandom必须产生不确定的输出。因此,传递给SecureRandom对象的任何种子材料都必须不可预测,并且所有SecureRandom输出序列必须具有加密强度,如RFC 4086:安全性的随机性要求中所述。

许多SecureRandom实现采用伪随机数生成器(PRNG,也称为确定性随机位生成器或DRBG)的形式,这意味着它们使用确定性算法从随机种子生成伪随机序列。其他实现可以产生真正的随机数,而其他实现则可以使用两种技术的组合。

补充随机数的知识

根据密码学原理,随机数的随机性检验可以分为三个标准:

  • 统计学伪随机性。统计学伪随机性指的是在给定的随机比特流样本中,1的数量大致等于0的数量,同理,“10”“01”“00”“11”四者数量大致相等。类似的标准被称为统计学随机性。满足这类要求的数字在人类“一眼看上去”是随机的。
  • 密码学安全伪随机性。其定义为,给定随机样本的一部分和随机算法,不能有效的演算出随机样本的剩余部分。
  • 真随机性。其定义为随机样本不可重现。实际上只要给定边界条件,真随机数并不存在,可是如果产生一个真随机数样本的边界条件十分复杂且难以捕捉(比如计算机当地的本底辐射波动值),可以认为用这个方法演算出来了真随机数。

相应的,随机数也分为三类:

  • 伪随机数:满足第一个条件的随机数。
  • 密码学安全的伪随机数:同时满足前两个条件的随机数。可以通过密码学安全伪随机数生成器计算得出。
  • 真随机数:同时满足三个条件的随机数。

SecureRandom比Random“强”在哪里

SecureRandom是强随机数生成器,它可以产生高强度的随机数,产生高强度的随机数依赖两个重要的因素:种子算法。算法是可以有很多的,通常如何选择种子是非常关键的因素。
Random的种子是 System.currentTimeMillis(),所以它的随机数都是可预测的, 是弱伪随机数。

强伪随机数的生成思路:收集计算机的各种信息,键盘输入时间,内存使用状态,硬盘空闲空间,IO延时,进程数量,线程数量等信息,CPU时钟,来得到一个近似随机的种子,主要是达到不可预测性
说的更通俗就是,使用加密算法生成很长的一个随机种子,让你无法猜测出种子,也就无法推导出随机序列数。

根据上面的补充知识,加上现在难以模拟真随机数,所以说Random顶多说是满足第一条件的“统计学伪随机数”,而SecureRandom强就强在,经过特殊处理后额外满足了第二条件即密码学安全伪随机性,可称为“密码学安全的伪随机数”,就会比普通统计学意义上的“伪随机数”不可预测一些,也就自然更安全、更强啦。

SecureRandom对象的生成

调用者通过无参数构造函数或getInstance方法之一获取SecureRandom实例。

例如:

  • SecureRandom r1 = new SecureRandom();
  • SecureRandom r2 = SecureRandom.getInstance(“NativePRNG”);
  • SecureRandom r3 = SecureRandom.getInstance(“DRBG”, DrbgParameters.instantiation(128, RESEED_ONLY, null));

上面的第三条语句返回支持特定实例化参数的特定算法的SecureRandom对象。实现的有效实例化参数必须匹配此最小请求,但不一定相同。例如,即使请求不需要某个功能,实际的实例也可以提供该功能。一个实现可以延迟地实例化SecureRandom,直到它被实际使用为止,但是有效的实例化参数必须在创建后立即确定,并且getParameters() 始终应返回不变的相同结果。

SecureRandom的一些使用

SecureRandom的典型调用者调用以下方法来检索随机字节:

  • SecureRandom random = new SecureRandom();
  • byte[] bytes = new byte[20];
  • random.nextBytes(bytes);

调用者还可以调用generateSeed(int)方法来生成给定数量的种子字节(例如,为其他随机数生成器提供种子):

  • byte[] seed = random.generateSeed(20);

不播种新创建的PRNG SecureRandom对象(除非它是由SecureRandom(byte [])创建的)。对nextBytes的首次调用将强制其从实现特定的熵源中播种自身。如果先前调用过setSeed,则不会发生这种自我播种。

通过调用reseed或setSeed方法,可以随时重新播种SecureRandom。重新设定种子的方法从其熵源读取熵输入以重新设定其自身的种子。 setSeed方法要求调用者提供种子。

请注意,并非所有SecureRandom实施都支持种子

一些SecureRandom实现可能在其nextBytes(byte [],SecureRandomParameters)和reseed(SecureRandomParameters)方法中接受SecureRandomParameters参数,以进一步控制这些方法的行为。

注意:根据实现的不同,例如,在各种类Unix操作系统上,如果熵源是/ dev / random,则在收集熵时,generateSeed,reseed和nextBytes方法可能会阻塞。

SecureRandom与并发编程

SecureRandom对象可安全用于多个并发线程。

通过在注册提供程序时将服务提供程序属性“ ThreadSafe”设置为“ true”,SecureRandom服务提供程序可以公告它是线程安全的。 否则,此类将改为同步对SecureRandomSpi实现的以下方法的访问:

  • SecureRandomSpi.engineSetSeed(byte[])
  • SecureRandomSpi.engineNextBytes(byte[])
  • SecureRandomSpi.engineNextBytes(byte[], SecureRandomParameters)
  • SecureRandomSpi.engineGenerateSeed(int)
  • SecureRandomSpi.engineReseed(SecureRandomParameters)

先不理SecureRandom啦

看点儿实际的~

尝试用System.currentTimeMillis()生成随机数

这种做法是:
利用System.currentTimeMillis(),获取从1970年1月1日0时0分0秒(这与UNIX系统有关,Java就这么搞的)到此刻的一个long型的毫秒数,取模之后即可得到所需范围内的随机数。

int max=100,min=1;
long randomNum = System.currentTimeMillis();  
int ran3 = (int) (randomNum%(max-min)+min);  
System.out.println(ran3);

Java随机数终章

本来没想写这么多内容的,写着写着就停不下来了,希望大家看到这里的时候看的还算满意。
起先对随机数这里不屑一顾,但慢慢的也觉得蛮有内容的。
Java的随机数,我们从Math.random(),到Random,再到ThreadLocalRandom,最后看到SecureRandom,可还真没那么简单呐。
更何况,如果真的深入剖析背后每一个设计的逻辑的话,就更难以理解了,但也是我们不断深入学习的体现啊!

另外,作为Java爱好者,我也很喜欢Python,嘿嘿……
之前有写过关于Python随机数的文章 → Here,用了LOL的一些内容,虽说不难可还蛮有趣的。

点个赞吧点个赞吧点个赞吧……
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

那就,ByeBye咯……
在这里插入图片描述

  • 4
    点赞
  • 0
    评论
  • 7
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值