在Java中,产生随机数的场景有很多种,所以在本文中,我们将盘点4种随机数的生成方式,以及它们之间的区别以及每种生成方式对应的场景。1、RandomRandom类诞生于JDK1.0,它产生的随机数是伪随机数,即规则随机数。Random使用的随机算法是linearcongruentialpseudorandomnumbergenerator(LGC)线性同余伪随机数。生成随机数时,随机算法的原点数称为种子数(seed),在种子数的基础上进行一定的变换,生成所需的随机数。当Random对象的种子数相同时,相同次数生成的随机数相同。例如,对于两个种子数相同的Random对象,第一次生成的随机数完全相同,第二次生成的随机数也完全相同。默认情况下,newRandom()使用当前纳秒时间作为种子数。①基本使用使用Random生成一个0到10(不含10)的随机数,实现代码如下://生成Random对象Randomrandom=newRandom();for(inti=0;i<10;i++){//生成0-9的随机整数intnumber=random.nextInt(10);System.out.println("Generaterandomnumber:"+number);}上面程序的执行结果为:②优缺点分析Random使用LGC算法生成伪随机数的优点在于执行效率比较高,生成速度比较快。它的缺点是如果Random的随机种子相同,则每次产生的随机数都是可预测的(都一样)。如下代码所示,当我们为两个线程设置相同的种子数时,会发现每次生成的随机数也是相同的://创建两个线程or(inti=0;i<2;i++){newThread(()->{//创建一个Random对象,设置相同的seedRandomrandom=newRandom(1024);//生成3个随机数for(intj=0;j<3;j++){//生成随机数intnumber=random.nextInt();//打印生成的随机数System.out.println(Thread.currentThread().getName()+":"+number);//休眠200mstry{Thread.sleep(200);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("--------------------");}}).start();上面程序的执行结果是:③线程安全问题当我们要使用一个类的时候,第一个关心的问题就是:它是线程安全的吗?对于Random,Random是线程安全的。PS:线程安全是指在多线程场景下,程序的执行结果与预期结果一致,称为线程安全,否则为非线程安全(也称为线程安全问题)。比如有两个线程,第一个线程执行100,000次++操作,第二个线程执行100,000次--操作,那么最后的结果应该既不加也不减。如果程序最终的结果不符合预期,那就不是线程安全的。我们看Random的实现源码:publicRandom(){this(seedUniquifier()^System.nanoTime());}publicintnextInt(){returnext(32);}protectedintnext(intbits){longoldseed,nextseed;AtomicLongseed=this.seed;do{oldseed=seed.get();nextseed=(oldseed*multiplier+addend)&mask;}while(!seed.compareAndSet(oldseed,nextseed));//CAS(CompareandSwap)生成随机数return(int)(nextseed>>>(48-bits));}PS:本文所有源码均来自JDK1.8.0_211。从上面的源码我们可以看出,Random底层使用了CAS(CompareandSwap,比较和替换)来解决线程安全问题。因此,对于绝大多数的随机数生成场景,并不缺少使用Random。这是一个不错的选择。PS:实现原子操作的Java并发机制有两种:一种是lock,一种是CAS。CAS是CompareAndSwap(比较和替换)的缩写。java.util.concurrent.atomic中的很多类,比如(AtomicIntegerAtomicBooleanAtomicLong等)都是使用CAS机制实现的。2.ThreadLocalRandomThreadLocalRandom是JDK1.7新提供的类。它是JUC(java.util.concurrent)的成员。为什么要在Random之后创建一个ThreadLocalRandom呢?原因很简单。我们通过上面Random的源码可以看出,Random在生成随机数的时候使用了CAS来解决线程安全问题。但是,在线程竞争激烈的场景下,CAS的效率非常低。原因是其他线程在CAS比较的时候一直在修改原来的值。所以CAS比较失败,只好一直循环尝试进行CAS操作。因此,在多线程竞争激烈的场景下,可以使用ThreadLocalRandom来解决Random执行效率低的问题。当我们第一次看到ThreadLocalRandom的时候,一定会想到一个类ThreadLocal,确实如此。ThreadLocalRandom的实现原理与ThreadLocal类似。相当于给每个线程一个自己的本地种子,从而避免了多个线程竞争一个种子带来的额外性能开销。①基本使用接下来,我们使用ThreadLocalRandom生成一个0到10(不包括10)的随机数。实现代码如下://获取ThreadLocalRandom对象ThreadLocalRandomrandom=ThreadLocalRandom.current();for(inti=0;i<10;i++){//生成0-9的随机整数intnumber=random.nextInt(10);//打印结果System.out.println("Generaterandomnumber:"+number);}上面程序的执行结果为:②实现原理ThreadLocalRandom的实现原理与ThreadLocal类似。它允许每个线程持有自己的本地种子,这些种子将在生成随机数时进行初始化。实现源码如下:publicintnextInt(intbound){//参数校验if(bound<=0)thrownewIllegalArgumentException(BadBound);//根据当前线程中的seed计算新的seedintr=mix32(nextSeed());intm=bound-1;//根据新的seed和bound计算随机数if((bound&m)==0)//poweroftwor&=m;else{//rejectover-representedcandidatesfor(intu=r>>>1;u+m-(r=u%bound)<0;u=mix32(nextSeed())>>>1);}returnr;}finallongnextSeed(){Threadt;longr;//读取和更新per-threadseed//获取当前线程中的threadLocalRandomSeed变量,然后在种子的基础上累加GAMMA值作为新的种子//使用UNSAFE.putLong设置新的种子存放在当前线程的threadLocalRandomSeed变量UNSAFE.putLong(t=Thread.currentThread(),SEED,r=UNSAFE.getLong(t,SEED)+GAMMA);returnr;}③ThreadL的优缺点ocalRandom结合了Random和ThreadLocal类,隔离在当前线程中,因此在多线程环境下通过避免操作种子数的竞争获得了更好的性能,也保证了它的线程安全。此外,与Random不同,ThreadLocalRandom明确不支持设置随机种子。它重写了Random的setSeed(longseed)方法,直接抛出UnsupportedOperationException,从而减少了多线程随机数重复的可能性。源码如下:publicvoidsetSeed(longseed){//onlyallowcallsfromsuper()constructorif(initialized)thrownewUnsupportedOperationException();}程序中只要调用setSeed()方法,就会抛出UnsupportedOperationException异常,如图下图中:ThreadLocalRandom劣势分析虽然ThreadLocalRandom不支持手动设置随机种子的方法,但并不代表ThreadLocalRandom是完美的。当我们查看ThreadLocalRandom的初始化随机种子的方法initialSeed()的源码时,我们发现默认情况下它的随机种子也是与当前时间相关的。源码如下:privatestaticlonginitialSeed(){//尝试获取JVM的启动参数Stringsec=VM.getSavedProperty("java.util.secureRandomSeed");//如果启动参数设置的值为true,该参数是一个随机的8位种子if(Boolean.parseBoolean(sec)){byte[]seedBytes=java.security.SecureRandom.getSeed(8);longs=(long)(seedBytes[0])&0xffL;for(inti=1;i<8;++i)s=(s<<8)|((long)(seedBytes[i])&0xffL);returns;}//如果没有设置启动参数,使用随机种子当前时间相关的算法secureRandomSeed=true",ThreadLocalRandom会生成一个随机种子,可以在一定程度上缓解随机种子。同样带来的可预测随机数的问题,但是默认情况下,如果不设置这个参数,那么在多线程中,可能是启动时间导致的3.SecureRandomSecureRandom继承自Random,它提供了一个密码学上强的随机数生成器。SecureRandom不同于Random,它收集一些随机事件,比如鼠标点击,键盘点击等。SecureRandom使用这些随机事件作为种子。这意味着种子是不可预测的,不像Random默认使用系统当前时间的毫秒数作为种子,从而避免了生成相同随机数的可能性。基本使用//创建一个SecureRandom对象并设置加密算法SecureRandomrandom=SecureRandom.getInstance("SHA1PRNG");for(inti=0;i<10;i++){//生成0-9的随机整数intnumber=random.nextInt(10);//打印结果System.out.println("Generaterandomnumber:"+number);}上面程序的执行结果为:SecureRandom默认支持两种加密算法:SHA1PRNG算法,由sun.security提供.供应商。安全随机;NativePRNG算法,提供者sun.security.provider.NativePRNG。当然,除了上述的操作方式,你还可以选择使用newSecureRandom()来创建一个SecureRandom对象。实现代码如下:SecureRandomsecureRandom=newSecureRandom();通过new初始化SecureRandom,默认使用NativePRNG算法生成随机数,也可以配置JVM启动参数“-Djava.security”参数修改生成随机数的算法,或者选择使用getInstance(“算法名称”)指定生成随机数的算法。4.MathMath类诞生于JDK1.0。它包含用于执行基本数学运算的属性和方法,例如基本指数、对数、平方根和三角函数。当然,它还包括产生随机数的静态方法Math。random(),此方法生成从0到1的双精度值,如以下代码所示。①基本使用for(inti=0;i<10;i++){//生成随机数doublenumber=Math.random();System.out.println("Generaterandomnumber:"+number);}以上的执行程序结果是:②扩展当然,如果你想用它生成一定范围的int值也是可以的,你可以这样写:for(inti=0;i<10;i++){//从0-99Integerintnumber=(int)(Math.random()*100)生成一个int值;System.out.println("Generaterandomnumber:"+number);}上面程序的执行结果为:③实现原理通过分析Math的源码,我们可以知道,当Math.random()方法第一次被调用时,会自动创建一个伪随机数生成器,它实际上使用了newjava.util.Random(),下次调用Math.random()方法,这个新的伪随机数生成器用来。源码如下:publicstaticdoublerandom(){returnRandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();}privatestaticfinalclassRandomNumberGeneratorHolder{staticfinalRandomNumberGenerator=newRandom();}小结本文介绍了四种生成随机数的方法,其中Math是封装随机的,如此相似。Random生成伪随机数,以当前纳秒级时间作为种子数,在多线程竞争激烈的情况下,因为CAS运算存在一定的性能问题,但是对于大多数应用场景来说,使用Random足够好了。在竞争激烈的场景下可以使用ThreadLocalRandom代替Random,但是如果对安全性要求比较高,可以使用SecureRandom来生成随机数,因为SecureRandom会收集一些随机事件作为随机种子,所以SecureRandom可以把它看成是一个工具用于生成真正随机数的类。
