Java生成随机数的8种常用方法及最佳实践详解
在 Java 中,生成随机数常用的写法有以下这些:总的来说,名称带有“安全”字样的不一定就安全,名字比较简洁的也不一定就简单。

Math.random()
语言的表达往往较为廉价,不如让我看到实际的代码。首先呈上原始的代码:
获取一个随机数。这个随机数是通过特定的方式生成的。具体的生成方式在该方法内部实现。它会返回一个表示随机数的双精度浮点数。
使用 RandomNumberGeneratorHolder 中的随机数生成器获取下一个双精度浮点数。即调用 RandomNumberGeneratorHolder.randomNumberGenerator 的 nextDouble 方法来获取随机数。
}
静态私有类 RandomNumberGeneratorHolder ,其中包含相关内容 ;该类用于特定的功能 ;在类内部有特定的结构和属性 ;用于生成随机数等相关操作 ;通过特定的方式进行初始化和使用 。
创建了一个名为 randomNumberGenerator 的静态 final 随机数生成器,它是通过 new Random() 来初始化的。
}
从源码能够得知,Math.random 实际上是使用 new Random。同时,它给出了一个最佳实践,即在使用时无需每次都创建一个新对象,直接将其做成一个静态类即可。
Random
Random 是伪随机数类,它是通过线性同余算法生成的。1946 年,冯诺依曼提出了伪随机数。当时他在参与一个氢弹制造项目,该项目需要大量随机数来模拟核聚变和核裂变。由于当时的设备无法存储大量随机数,所以他想到了这个办法。之所以称其为伪随机数(或伪乱数),是因为真随机数都不是通过计算得出的。这里计算出的数序看似随机,但其实是使用确定性算法得出的。所以伪随机数并非真正随机。从更宏观的角度看,它是很有规律的,就如同我在《大话高可用》中提及的炊烟那样。
下面来讲讲这个线性同余算法,大家无需担心。我对这个算法的了解程度比大家还要少。不过我具备人类的常识,那这个常识是什么呢?因为是通过计算得出的,并且在人类看来没有规律,所以必定存在一个外部因子(种子),而种子是一个变量。接着看源码:
/**
将随机数生成器的种子设置为一个很可能的值。
与这个构造函数的任何其他调用都有所不同。
*/
public Random() {
seedUniquifier() 与 System.nanoTime() 进行异或运算,得到这个结果。
}
在构造函数中给出了答案,这个因子是系统时间,且精确到纳秒。为了方便描述,我们举例说明,比如想要一个 int 类型的随机数(实际上它可以生成其他数值类型的随机数)。现在来看看源码:
public int nextInt() {
return next(32);
}
获取的下一个值的类型为 protected int ,参数是 bits 。
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
oldseed 乘以 multiplier 加上 addend 所得结果与 mask 按位与得到 nextseed ;
使用 `seed.compareAndSet(oldseed, nextseed)` 进行循环判断,只要不满足条件就继续循环,直到满足条件为止。在循环过程中,不断更新 `oldseed` 和 `nextseed` 的值,以确保循环的正确性和准确性。
返回将 nextseed 向右移动 48 - bits 位后取整的结果,该结果为整数类型。
}
解释一下:调用 nextInt 时,通常会生成 32 位的数字。调用 next 方法,会产生 oldseed 和 nextseed 。通过计算 nextseed ,会进行 CAS 操作,用 nextseed 替换现有的 oldseed 。nextseed 的计算方式是对 oldseed 进行一些运算,而这些运算的其他数值都是常量。最终会返回 nextseed 的值。
使用了 AtomicLong 以及自旋 + CAS(可参考《系统梳理一下锁》),这使得 Random 生成随机数具备了线程安全性。新建一个全局变量,之后就无需再次新建,而 Math.random 的调用方法是合理的。
大胆猜测一下:若有两个不同的进程或线程,在纳秒级的相同时间同时去调用 new Random ,那么此时 nextInt 的返回值会是相同的!并且下次调用 nextInt 的值也会相同!若要进行验证,可下载 java 源码,在 new Random 的位置稍作改动。
将 System.nanoTime()修改为固定值,对于这一点我不进行验证。
ThreadLocalRandom
上面提到 Random 使用了两种算法,一是线程安全的 AtomicLong,二是自旋加 CAS。这两种算法在保证线程安全的同时,带来了很大的性能开销。ThreadLocalRandom 是 JDK 7 之后提供的用于并发产生随机数的方式,它能够解决多个线程之间发生的竞争问题。
它并非通过 new 进行实例化,而是在首次使用时对其静态方法 current()进行初始化。若直接调用静态方法,能够让每个线程实例化一个。有朋友进行过测试,在 100 个线程的情形下,经过 3 分钟,Math.random 能够运行几百次。并且
ThreadLocalRandom.current()能够产生的随机数次数可以达到十几万次。
来看一下它是怎么做到的:
具体来说,通过调用这个静态方法可以得到一个与当前线程相关联的线程本地随机数生成器对象,它可以在多线程环境下为每个线程提供独立的随机数序列,避免了多线程之间的竞争和冲突。
如果(Thread.currentThread() 获取的 UNSAFE 的整数值为 0)
localInit();
return instance;
}
这些操作是在本地范围内进行的。
probeGenerator 执行了一个操作,将一个值增加并获取结果,这个增加的值为 PROBE_INCREMENT,最终的结果赋值给了变量 p 。
如果 p 等于 0,那么 probe 等于 1;否则,probe 等于 p。 // 跳过 0
然后这个结果再经过 mix64 函数处理后赋值给 long seed。
获取当前正在运行的线程赋值给变量 t 。
UNSAFE 将一个长整型数值 seed 放入到 t 中,具体是放入到 SEED 位置。
UNSAFE 将整数 probe 放入到 t 中的 PROBE 位置。
}
简而言之,Random 借助系统时间这一外部因子,而 ThreadLocalRandom 则通过 UNSAFE 调用本地方法,获取线程本身的一些变量作为外部因子。所有参数都绑定在线程自身上,与其他线程不存在竞争关系,因此无需加锁就能保证线程安全。
public int nextInt() {
return mix32(nextSeed());
其中,z 为一个长整型数值。
z 先将自身右移 33 位(z >>> 33),然后与自身进行异或运算(z ^ 该结果),最后乘以 0xff51afd7ed558ccdL 。
计算 ((z ^ (z 右移 33 位)) * 0xc4ceb9fe1a85ec53L) 后将结果右移 32 位并转换为整数返回
}
final long nextSeed() {
Thread t; a long r; // 用于读取和更新每个线程的种子
Thread.currentThread() 被赋值给 t,SEED 被赋值给 UNSAFE 的一个 long 类型变量,然后进行相关操作。
r 等于 UNSAFE 获取 t 对应的长整型值,再加上 GAMMA ;
return r;
}
生成随机数进行计算时,调用的都是线程内的变量,并且没有加锁。大家可以由此类推,回忆一下关于 ThreadLocal 的相关知识。
SecureRandom
在安全的应用场景中,需要使用安全的随机数。对于密码学意义而言,安全随机数必须要保证其具有不可预测性。
Random 以时间作为随机数种子。若黑客知晓随机数产生的时间,便可重现随机数。SecureRandom 属于强随机数,通常不会仅以时间作为随机数种子,除了系统时间外,还会以临时文件夹中的大小、某个线程从休眠到被唤醒所耗费的时间等一系列无法重现的值作为随机数种子。
SecureRandom 由于采用了诸多外部参数,所以会出现熵源不足时阻塞的情况。在我所参与过的项目里,由于部分业务在凌晨时段没有流量,这个问题实际上是曾经发生过的。建议对于有明显低谷的业务,当低谷时的业务量低于 1TPS 时,就不要使用 SecureRandom 。
强随机如 SecureRandom 在很多场景下效果未必好。有个小故事讲,itunes 以前播歌时采用的是真随机。有些用户总是抱怨,说这是真随机吗?怎么老是给他们播放那几首歌。苹果公司有苦却难以言说,于是将算法改成了伪随机,即洗牌算法,先把所有歌打乱顺序,再按顺序播放。改完后用户纷纷点赞。
适合使用 SecureRandom 的场景有,例如要给每个资源生成一个 url 时,为避免 url 规律被他人攻破,在使用爬虫爬取自己的资源时可以运用此方法。
UUID.randomUUID()
从下面的代码可以得知 UUID.randomUUID()是以 SecureRandom 为基础的。
获取一个随机的 UUID。这个 UUID 是通过特定的算法生成的,用于在分布式系统中唯一标识一个对象或实体。它具有全局唯一性,即在不同的系统和时间点生成的 UUID 几乎不可能重复。可以使用这个方法来创建一个新的随机 UUID 实例。
Holder 的 numberGenerator 被赋值给了 SecureRandom 类型的 ng 。
创建了一个长度为 16 的字节数组 randomBytes 。
ng.nextBytes(randomBytes);
randomBytes[6] 与 0x0f 进行按位与操作,目的是清除版本信息。
randomBytes[6] 进行按位或操作,将其与 0x40 进行或运算;这是为了将其设置为版本 4。
randomBytes[8] 与 0x3f 进行按位与运算;这是为了清除变体相关的位;通过这种方式对 randomBytes[8] 进行了特定的操作以达到清除变体的目的。
randomBytes 的前 8 个字节进行按位或操作,将其与 0x80 进行或运算;这是为了设置成 IETF 变体。
return new UUID(randomBytes);
}
有的朋友提出异议,称在网上得知 UUID 是由时间戳、时钟序列以及 MAC 地址组成的。并且要注意到上述代码注释中存在一个设置为版本 4 的内容。
从UUID的类注释里就可以看到java 8提到4个版本:
UUID 有四种不同的基本类型:基于时间的、DCE 类型的。
安全相关的、基于名称的以及随机生成的 UUIDs。这些类型具有一种
版本值分别为 1、2、3 和 4。
翻译一下:
版本1-基于时间
版本2 -基于DCEsercrity
版本3 -基于名字(MD5)
版本4 -基于随机数
记住啦:java默认UUID是基于随机数的!
证明一下:如果是基于时间戳的,生成的UUID前几位应该相同
@Test
public void test() {
for (int i = 0; i < 3; i++) {
System.out.println(UUID.randomUUID());
}
}
结果:
该句子只是一个唯一标识的字符串,没有具体的语义内容,无法进行改写。如果你能提供具体的句子或情境,我将很乐意帮助你进行改写。
该字符串为一个特定的 UUID,由不同部分组成,分别是 17ebdc58、db6c、452f、9b21、b4f54d3958a5 等,它们共同构成了这个唯一的标识。
此为一个特定的标识或代码,由多个部分组成,分别是 116d6a4f、e50a、4001、b94c 和 61596da3a75d,它们共同构成了这个独特的整体。
事实胜于记忆中的知识,它有可能是错的!
java UUID也支持版本3:
@Test
public void test() {
for (int i = 0; i < 3; i++) {
System.out.println(UUID.nameUUIDFromBytes("编程一生".getBytes()));
}
}
运行结果:
它具有独特的性质和特点,在特定的领域和情境中发挥着重要的作用,对相关的事物和现象产生着影响,展现出其独特的价值和意义。
743f24b2-0914-363b-ace1-f4da750dccad
743f24b2-0914-363b-ace1-f4da750dccad
这个在你的机器上执行也是这个结果,就是MD5了一下。
有人说如何使用版本 1 和版本 2 ,似乎还听闻过版本 5 。能够使用 linux 命令 。
uuid -n 3-v1
运行结果:
这是一个特定的标识符,由 32 位字符组成,其中包含字母和数字。它的前两位是 5b,代表着某种特定的含义或系统。后面的部分依次是 01cea2、9561、11e9、965b 和 a3d050dd0f23,每个部分都有其独特的作用和意义。这个标识符在特定的环境或系统中被使用,用于标识或区分不同的对象、事件或数据等。它就像一个独特的标签,能够准确地定位和识别相关的信息。
请提供具体需要改写的句子呀,你只给了一个 UUID 呢,没有具体的句子内容呢。
请明确具体的句子内容呀,没有看到横线下的句子呢。
linux命令版本可指定!
RandomStringUtils 以及 RandomUtils 和 RandomUtil
这两个方法并非 Java 原生的。在 apache commons 中存在这两个工具类,其本质依然是 Random。通过跟进代码能够发现:RandomUtils 使用的是 JVMRandom。它只是对一个静态的 Random 进行了封装,凡是使用 RandomUtils 类的,在全局范围内都只使用一个,这样就减少了新建对象的成本。
如果你问我通常情况下建议使用哪一个,我会选择 ThreadLocalRandom。那么这样优秀的方法是不是应该也有与之对应的封装工具类呢?答案是有的,hultool 已经创建了一个 RandomUtil。
RandomUtil.getRandom()所返回的就是 ThreadLocalRandom。
RandomUtil.getSecureRandom()所返回的是 SecureRandom,并且它能提供多种选择。
总结
一张图表示他们之间的关系:
在《CURD 系统怎么做出技术含量--怎样引导面试》中,我提及能够运用工作中总结出来的技巧给面试官留下深刻印象。倘若能够将一个问题系统地讲解清楚,甚至使面试官察觉到之前的知识存在错误,那么必定能够让面试官眼前一亮。