在 Java 语言开发里,处理业务逻辑时我们经常会遇到概率问题,需借助合适的方法生成随机数来满足需求。 Java为我们提供了两种常用生成随机数的方式,一是Math.random()方法,二是借助java.util.Random。
通过查看源码可知 Math.random() 是对 Random() 的进一步封装。
Random 是依据算法,生成一个“伪随机数”,在种子(seed)一样的情况下,每次生成的随机数是有规律,有迹可循的。若不设置seed的情况下,系统将由算法自动生成一个seed值,虽然这种方式每次生成的seed值不同,但其方法较为简单,依旧是有规律可循,安全性并不高。
由 Random 生成的随机数是线程安全的,Random类的线程安全性依赖于内部的AtomicLong原子变量(存储种子),其核心方法next(int bits)通过CAS(Compare-And-Swap)操作更新种子:虽然CAS保证了种子更新的原子性,但在高并发下,多个线程可能频繁重试CAS,导致性能下降。
为了满足多线程下随机数的高效获取,JDK 引入了专为多线程设计的随机数生成器Thread LocalRandom,每个线程维护独立的种子(seed),完全避免多线程之间种子(seed)的竞争。ThreadLocalRandom采用无锁设计,每个线程通过ThreadLocal持有自己的随机种子,保证了在高并发下比Random更加高效。
通过源码可知,ThreadLocalRandom 是 Random 子类,使用方法与Random相差不大。
以上三种获取随机数的方式,实则都是通过算法 + 种子设定生成“伪随机数”,而非“真随机数”。这里我们明确两个基本概念:真随机数(True Random Number,TRN),伪随机数(Pseudo-Random Number,PRN),“真随机数”需要满足以下几个条件,缺一不可。
1. 不可预测性
无法通过历史数据或算法推测后续的随机数值。即使攻击者已知部分随机数序列,也无法推导出后续数值。
2. 统计随机性(Statistical Randomness)
均匀分布:数值在可能的取值范围内分布均匀,无统计偏差。例如,抛硬币时正反面出现概率严格趋近50%。
3. 独立性(Independence)
无序列关联,生成的每个随机数与前后数值无任何相关性。例如:抛骰子时前一次结果为6,下一次出现6的概率仍为1/6。
4. 不可重复性(Non-reproducibility)
无法复现相同序列,即使初始条件完全相同,也无法生成两次完全一致的随机数序列。
5. 物理熵源依赖(Physical Entropy Source)
必须依赖物理世界的不可预测事件(如抛出的硬币,掷出的骰子,热噪声、光子行为、电磁干扰等)。
现实世界中,借助物理条件,获取一个随机数十分容易。如借助硬币,可以得到正反两种可能。也就是生成 0,1两种可能的随机数。
通过投掷骰子,可以得到数字1~6的随机数。
通过转盘,标注数字的方式,获取一个更大范围的随机数。
现实世界中,随着获取的随机数数值范围越大,难度也会随之增加。例如生成一个1 ~ 10000内的随机数,难度将会变得很大,同时,获取效率也会变低。
在计算机系统中,在没有生成随机数物理设备的支持下,通常会选择使用算法,获取一个“伪随机数”。“伪随机数”仅依赖算法和初始种子,与物理熵源无关。
伪随机数(PRNG, Pseudo-Random Number)满足几下几点:
生成原理
基于确定性算法:通过数学公式和初始种子生成数列。
可预测性:若知道算法和种子,可完全重现序列。
特点
周期性:所有PRNG最终会重复输出序列(周期长度因算法而异)。
高效性:生成速度快,适合大规模计算。
统计特性良好:设计优秀的PRNG能通过随机性测试(如均匀分布、独立性)。
“伪随机数”因其高效性,在计算机编程中被大量使用。例如各种软件登录中生成的验证码,游戏开发中地图生成、NPC行为。“真随机数”与“伪随机数”对比如下:
前文提到的Random,Math.random(),ThreadLocalRandom 都是基于算法,不依赖于物理熵源的“伪随机数”,满足了大多数编程需求。相对的,“伪随机数”因为由算法生成,具有周期性,可预测性的特点,导致安全性并不高。对于安全性更高的需求,Java同样提供了相应的方式——SecureRandom,由名字便知,这是个安全性更高的随机数。我们先来看下其用法。
SecureRandom在构造方法中同样提供了一个seed值,通过案例代码可知,这个seed值是byte数组形式,相较于前面三种随机数seed数值设置整数值,区别较大。SecureRandom无参构造函数中,也会默认设置一个seed值,为0。设置固定,或由算法生成seed值,是“伪随机数”的显著特征。那么SecureRandom是“真随机数”还是“伪随机数”呢?接着进入其源码一探究竟。
在其构造函数源码中,可以看到函数 getDefaultPRNG(true, seed),至此,我们可以明确的得知,这是一个“伪随机数”,(PRNG, Pseudo-Random Number Generator )
同样是伪随机数,为何冠以“Secure”之名呢?SecureRandom 类用于生成加密安全的随机数。与 Random 类不同,SecureRandom 专为加密应用而设计,它提供了更强的随机数生成算法,如SHA1PRNG、NativePRNG等。这些算法通常基于密码学安全的伪随机数生成器(PRNGs),这些生成器在多次迭代后仍然能够保持难以预测,同时SecureRandom借助系统的熵源,像鼠标移动、磁盘 I/O 时间等物理事件来初始化种子。虽然这些熵源具有一定随机性,但本质上仍是从系统状态获取信息作为seed值,进而依据算法生成随机数,不是直接来自物理随机过程。SecureRandom生成的随机数在密码学领域是安全的,可满足加密、签名等对随机性要求高的场景,可被认为是一个“准真随机数”,但其理论上仍是一个“伪随机数”,这点必须明确。
例如使用SecureRandom 在用户注册,登录功能上,为密码哈希生成唯一的盐值,防止彩虹表攻击。AI时代,我们当然无需手动实现。使用飞算JavaAI 智能问答,输入指令:用户注册时,使用SecureRandom为密码哈希生成唯一的盐值,防止彩虹表攻击。
飞算JavaAI 根据简单需求,进行分析,并给出解决方案。
接着给出完成代码实现。
并在后面给出详细的代码解释,使整个“分析-实现-解释”流程一目了然。AI辅助编程,减少代码错误的同时,总是给到我们更多。
那么Java如果要生成一个真正意义上的随机数该如何实现?答案是,对于开发者来说,现阶段无法实现。在计算机系统中,若需严格真随机数,需外接硬件设备(如量子随机数生成器),并通过JNI/JNA调用其API,在Java标准库中不直接支持此类硬件。所以在Java程序中生成一个“真随机数”,其难度远远超过我们的想象。开发环境下,随机数往往并不追求 “真随机数”,只要满足数据相对公平即可,如篮球游戏中投篮命中率算法,使用“伪随机数”对每个玩家都是公平的。
例如,我们需要生成一个安全性更高的4位数验证码,这个时候“安全性更高”通常不会使用到SecureRandom,只需在验证码复杂度上稍作优化便可。大家可以回忆一下,是否很少收到有重复性(1121),规律性(1234,2020)的验证码。将需求交给飞算JavaAI:生成一个四位数验证码,安全系数更高的。
生成代码如下:
也可优化指令,飞算JavaAI 将根据更加明确的指令生成符合需求的代码。
最后,Java四种生成随机数使用场景总结如下。