更多精彩文章。java
《微服务不是所有,只是特定领域的子集》linux
最有用系列:bash
《Linux生产环境上,最经常使用的一套“vim“技巧》多线程
《Linux生产环境上,最经常使用的一套“Sed“技巧》并发
咱们从jdk8提及。主要是四个随机数生成器。神马?有四个?
接下来咱们简单说下这几个类的使用场景,来了解其中的细微差异,和api设计者的良苦用心。
java.util.Random
java.util.concurrent.ThreadLocalRandom
java.security.SecureRandom
java.util.SplittableRandom
复制代码
最经常使用的就是Random。
用来生成伪随机数
,默认使用48
位种子、线性同余公式
进行修改。咱们能够经过构造器传入初始seed
,或者经过setSeed重置(同步)。默认seed为系统时间的纳秒数,真大!
若是两个(多个)不一样的Random实例,使用相同的seed,按照相同的顺序调用相同方法,那么它们获得的数字序列也是相同的。这看起来不太随机。 这种设计策略,既有优势也有缺点,优势是“相同seed”生成的序列是一致的,使过程具备可回溯和校验性(平台无关、运行时机无关);缺点就是,这种一致性,潜在引入其“可被预测”的风险。
Random的实例是线程安全的。 可是,跨线程并发使用相同的java.util.Random实例可能会遇到争用,从而致使性能稍欠佳(nextX方法中,在对seed赋值时使用了CAS,测试结果显示,其实性能损耗很小)。 请考虑在多线程设计中使用ThreadLocalRandom。同时,咱们在并发环境下,也没有必要刻意使用多个Random实例。
Random实例不具备加密安全性。 相反,请考虑使用SecureRandom来获取加密安全的伪随机数生成器,以供安全敏感应用程序使用。
Random是最经常使用的随机数生成类,适用于绝大部分场景。
Random random = new Random(100);
System.out.println(random.nextInt(10) + "," + random.nextInt(30) + "," + random.nextInt(50));
random = new Random(100);
System.out.println(random.nextInt(10) + "," + random.nextInt(30) + "," + random.nextInt(50));
random = new Random(100);
System.out.println(random.nextInt(10) + "," + random.nextInt(30) + "," + random.nextInt(50));
复制代码
上述三个不一样的random实例,使用了相同的seed。调用过程同样,其中产生的随机数序列也是彻底同样的。屡次执行结果也彻底一致,简单而言,只要初始seed同样,即便实例不一样,屡次运行它们的结果都是一致的。这个现象与上面所说的一致。
若是Random构造器中不指定seed,而是使用默认的系统时间纳秒数做为主导变量,三个random实例执行的结果是不一样的。屡次执行结果也不同。因而可知,seed是否具备随机性,在必定程度上,也决定了Random产生结果的随机性。
因此,在分布式或者多线程环境下,若是Random实例处于代码一致的tasks线程中,可能这些分布式进程或者线程,产出的序列值是同样的。这也是在JDK 7引入ForkJoin的同时,也引入了ThreadLocalRandom类。
这个类的做用,使得随机数的生成器隔离到当前线程。此类继承自java.util.Random,与Math类使用的全局Random生成器同样,ThreadLocalRandom使用内部生成的种子进行初始化,不然可能没法修改。
在并发程序中使用ThreadLocalRandom,一般会有更少的开销和竞争。 当多个任务(例如,每一个ForkJoinTask)在线程池中并行使用随机数时,ThreadLocalRandom是特别合适的。
切记,在多个线程中不该该共享ThreadLocalRandom实例。
ThreadLocalRandom初始化是private的,因此没法经过构造器设定seed,此外其setSeed方法也被重写而不支持(抛出异常)。默认状况下,每一个ThreadLocalRandom实例的seed主导变量值为系统时间(纳秒):
private static long initialSeed() {
String sec = VM.getSavedProperty("java.util.secureRandomSeed");
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()));
}
复制代码
根据其初始化seed的实现,咱们也能够经过JVM启动参数增长“-Djava.util.secureRandomSeed=true”,此时初始seed变量将再也不是系统时间,而是由SecureRandom类生成一个随机因子,以此做为ThreadLoalRandom的初始seed。
真是够绕的。
从源码中,我并无看到Thread-ID做为变量生成seed,并且nextX方法中随机数生成算法也具备一致性。这意味着,若是多个线程初始ThreadLocalRandom的时间彻底一致,在调用方法和过程相同的状况下,产生的随机序列也是相同的;在必定程度上“-Djava.util.secureRandom=true”能够规避此问题。
ThreadLocalRandom并无使用ThreadLocal来支持内部数据存储等,而是直接使用UnSafe操做当前Thread对象引用中seed属性的内存地址并进行数据操做,我比较佩服SUN的这种巧妙的作法。
它也继承自Random,该类提供加密强随机数生成器(RNG),加密强随机数最低限度符合FIPS 140-2“加密模块的安全要求”。 此外,SecureRandom必须产生非肯定性输出。 所以,传递给SecureRandom对象的任何种子材料必须是不可预测的,而且全部SecureRandom输出序列必须具备加密强度。(官文,其实我也只知其一;不知其二)
SecureRandom默认支持两种RNG加密算法实现:
1)"SHA1PRNG"算法提供者sun.security.provider.SecureRandom
2)"NativePRNG"提供者sun.security.provider.NativePRNG
默认状况下,是“SHA1PRNG”,即SUN提供的实现。此外能够经过“-Djava.security=file:/dev/urandom”(推荐)或者“-Djava.security=file:/dev/random”指定使用linux本地的随机算法,即NativePRNG;其中“/dev/random”与“/dev/urandom”在不一样unix-*平台中实现有所不一样,性能也有所差别,建议使用“/dev/urandom”。
/dev/random的一个副本是/dev/urandom (”unlocked”,非阻塞的随机数发生器),它会重复使用熵池中的数据以产生伪随机数据。这表示对/dev/urandom的读取操做不会产生阻塞,但其输出的熵可能小于/dev/random的。它能够做为生成较低强度密码的伪随机数生成器,不建议用于生成高强度长期密码。
算法的内部实现,比较复杂;本人测试,其实性能差不不太大(JDK 8环境)。SecureRandom也是线程安全的。
从输出结果上分析,不管是否指定SecureRandom的初始seed,单个实例屡次运行的结果也彻底不一样 ;多个不一样的SecureRandom实例不管是否指定seed,即便指定同样的初始seed,同时运行的结果也彻底不一样。
SecureRandom继承自Random,可是对nextX方法中的底层方法进行的重写覆盖,不过仍然基于Random的CAS且SecureRandom的底层方法还使用的同步,因此在并发环境下,性能比Random差了一些。
JDK 8 新增的API,主要适用于Fork/join形式的跨线程操做中。它并无继承java.util.Random类。
具备相同seed的不一样SplittableRandom实例或者同一个SplittableRandom,屡次运行结果是一致的。这和Random是一致的。
非线程安全,不能被并发使用。 (不会报错,可是并发时可能多个线程同时获得相同的随机数)
同ThreadLocalRandom,对“-Djava.util.secureRandom=true”参数支持,可是只有使用默认构造器的时候,才会使用SecureRandom辅助生成初始seed。即不指定初始seed时,同一个SplittableRandom实例屡次运行,或者不一样的实例运行,结果是不一样的。
其中有一个split()方法,用来构造并返回与新的实例,这个实例共享了一些不可变状态。须要注意,split产生的新SplittableRandom实例,与原实例并不存在内部数据的并发竞争,也不会交替或者延续原实例的随机数生成序列(即两个实例产出随机序列的一致性,与原实例没有关系,只是在统计值层面更加接近);可是代码一致性的状况下,屡次运行,其随机数序列的结果老是一致的(假如初始seed是指定的,而非默认),这一点与Random、ThreadLocalRandom表现相同。
public SplittableRandom split() {
return new SplittableRandom(nextLong(), mixGamma(nextSeed()));
}
复制代码
样例代码。
System.out.println("第一段");
SplittableRandom random = new SplittableRandom(100);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SplittableRandom _random = random.split();
for (int i=0; i < 5; i++) {
System.out.println("---" + _random.nextInt(100));
}
}
});
thread.start();
thread.join();
for (int i=0; i < 5; i++) {
System.out.println("+++" + random.nextInt(100));
}
System.out.println("第二段");
SplittableRandom _random = new SplittableRandom(100);
for (int i=0; i < 10; i++) {
System.out.println("..." + _random.nextInt(100));
}
复制代码
执行结果。
第一段
---71
---85
---10
---60
---98
+++44
+++87
+++77
+++67
+++72
第二段
...92
...30
...44
...87
...77
...67
...72
...23
...9
...64
复制代码
从执行结果上看,split产生的random实例与原实例执行结果上没有类似之处;可是不一样SplittableRandom实例(不管是否执行过split),其产出随机数序列是一致的。
简析,基准:100000随机数,单线程
一、 Random :2毫秒
二、 ThreadLocalRandom :1毫秒
三、 SecureRandom
1)默认算法,即SHAR1PRNG:80毫秒左右。
2)NativePRNG:90毫秒左右。
四、 SplittableRandom :1毫秒
日常使用Random,或者大多数时候使用,都是没有问题的,它也是线程安全的。SplittableRandom是内部使用的类,应用较少,即便它也是public的也掩饰不了偏门。ThreadLocalRandom是为了在高并发环境下节省一点细微的时间,追求性能的应用推荐使用。而对于有安全需求的,又但愿更随机
一些的,用SecureRandom再好不过了。
jdk居然有这么多随机数生成器。有没有大吃一精?我反正是跪了。
更多精彩文章。