java生成随机数代码Java中生成随机数的4种方式




java生成随机数代码Java中生成随机数的4种方式

2022-07-21 2:27:06 网络知识 官方管理员

在Java中,生成随机数的场景有很多,所以本文我们就来盘点一下4种生成随机数的方式,以及它们之间的区别和每种生成方式所对应的场景。

1.Random

Random类诞生于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("生成随机数:"+number);}

以上程序的执行结果为:

java生成随机数代码(Java中生成随机数的4种方式)(1)

②优缺点分析

Random使用LGC算法生成伪随机数的优点是执行效率比较高,生成的速度比较快

它的缺点是如果Random的随机种子一样的话,每次生成的随机数都是可预测的(都是一样的)。如下代码所示,当我们给两个线程设置相同的种子数的时候,会发现每次产生的随机数也是相同的:

//创建两个线程for(inti=0;i<2;i++){newThread(()->{//创建Random对象,设置相同的种子Randomrandom=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();}

以上程序的执行结果为:

java生成随机数代码(Java中生成随机数的4种方式)(2)

③线程安全问题

当我们要使用一个类时,我们首先关心的第一个问题是:它是否为线程安全?对于Random来说,Random是线程安全的

PS:线程安全指的是在多线程的场景下,程序的执行结果和预期的结果一致,就叫线程安全的,否则则为非线程安全的(也叫线程安全问题)。比如有两个线程,第一个线程执行10万次++操作,第二个线程执行10万次--操作,那么最终的结果应该是没加也没减,如果程序最终的结果和预期不符,则为非线程安全的。

我们来看Random的实现源码:

publicRandom(){this(seedUniquifier()^System.nanoTime());}publicintnextInt(){returnnext(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并发机制实现原子操作有两种:一种是锁,一种是CAS。

CAS是CompareAndSwap(比较并替换)的缩写,java.util.concurrent.atomic中的很多类,如(AtomicIntegerAtomicBooleanAtomicLong等)都使用了CAS机制来实现。

2.ThreadLocalRandom

ThreadLocalRandom是JDK1.7新提供的类,它属于JUC(java.util.concurrent)下的一员,为什么有了Random之后还会再创建一个ThreadLocalRandom?

原因很简单,通过上面Random的源码我们可以看出,Random在生成随机数时使用的CAS来解决线程安全问题的,然而CAS在线程竞争比较激烈的场景中效率是非常低的,原因是CAS对比时老有其他的线程在修改原来的值,所以导致CAS对比失败,所以它要一直循环来尝试进行CAS操作。所以在多线程竞争比较激烈的场景可以使用ThreadLocalRandom来解决Random执行效率比较低的问题

当我们第一眼看到ThreadLocalRandom的时候,一定会联想到一次类ThreadLocal,确实如此。ThreadLocalRandom的实现原理与ThreadLocal类似,它相当于给每个线程一个自己的本地种子,从而就可以避免因多个线程竞争一个种子,而带来的额外性能开销了

java生成随机数代码(Java中生成随机数的4种方式)(3)

①基础使用

接下来我们使用ThreadLocalRandom来生成一个0到10的随机数(不包含10),实现代码如下:

//得到ThreadLocalRandom对象ThreadLocalRandomrandom=ThreadLocalRandom.current();for(inti=0;i<10;i++){//生成0-9随机整数intnumber=random.nextInt(10);//打印结果System.out.println("生成随机数:"+number);}

以上程序的执行结果为:

java生成随机数代码(Java中生成随机数的4种方式)(4)

②实现原理

ThreadLocalRandom的实现原理和ThreadLocal类似,它是让每个线程持有自己的本地种子,该种子在生成随机数时候才会被初始化,实现源码如下:

publicintnextInt(intbound){//参数效验if(bound<=0)thrownewIllegalArgumentException(BadBound);//根据当前线程中种子计算新种子intr=mix32(nextSeed());intm=bound-1;//根据新种子和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;//readandupdateper-threadseed//获取当前线程中threadLocalRandomSeed变量,然后在种子的基础上累加GAMMA值作为新种子//再使用UNSAFE.putLong将新种子存放到当前线程的threadLocalRandomSeed变量中UNSAFE.putLong(t=Thread.currentThread(),SEED,r=UNSAFE.getLong(t,SEED)+GAMMA);returnr;}

③优缺点分析

ThreadLocalRandom结合了Random和ThreadLocal类,并被隔离在当前线程中。因此它通过避免竞争操作种子数,从而在多线程运行的环境中实现了更好的性能,而且也保证了它的线程安全

另外,不同于Random,ThreadLocalRandom明确不支持设置随机种子。它重写了Random的

setSeed(longseed)方法并直接抛出了UnsupportedOperationException异常,因此降低了多个线程出现随机数重复的可能性

源码如下:

publicvoidsetSeed(longseed){//onlyallowcallfromsuper()constructorif(initialized)thrownewUnsupportedOperationException();}

只要程序中调用了setSeed()方法就会抛出UnsupportedOperationException异常,如下图所示:

java生成随机数代码(Java中生成随机数的4种方式)(5)

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;}//如果没有设置启动参数,则使用当前时间有关的随机种子算法return(mix64(System.currentTimeMillis())^mix64(System.nanoTime()));}

从上述源码可以看出,当我们设置了启动参数“-Djava.util.secureRandomSeed=true”时,ThreadLocalRandom会产生一个随机种子,一定程度上能缓解随机种子相同所带来随机数可预测的问题,然而默认情况下如果不设置此参数,那么在多线程中就可以因为启动时间相同,而导致多个线程在每一步操作中都会生成相同的随机数

3.SecureRandom

SecureRandom继承自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("生成随机数:"+number);}

以上程序的执行结果为:

java生成随机数代码(Java中生成随机数的4种方式)(6)

SecureRandom默认支持两种加密算法:

  1. SHA1PRNG算法,提供者sun.security.provider.SecureRandom;
  2. NativePRNG算法,提供者sun.security.provider.NativePRNG。

当然除了上述的操作方式之外,你还可以选择使用newSecureRandom()来创建SecureRandom对象,实现代码如下:

SecureRandomsecureRandom=newSecureRandom();

通过new初始化SecureRandom,默认会使用NativePRNG算法来生成随机数,但是也可以配置JVM启动参数“-Djava.security”参数来修改生成随机数的算法,或选择使用getInstance("算法名称")的方式来指定生成随机数的算法。

4.Math

Math类诞生于JDK1.0,它里面包含了用于执行基本数学运算的属性和方法,如初等指数、对数、平方根和三角函数,当然它里面也包含了生成随机数的静态方法Math.random(),此方法会产生一个0到1的double值,如下代码所示。

①基础使用

for(inti=0;i<10;i++){//产生随机数doublenumber=Math.random();System.out.println("生成随机数:"+number);}

以上程序的执行结果为:

java生成随机数代码(Java中生成随机数的4种方式)(7)

②扩展

当然如果你想用它来生成一个一定范围的int值也是可以的,你可以这样写:

//创建两个线程for(inti=0;i<2;i++){newThread(()->{//创建Random对象,设置相同的种子Randomrandom=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();}0

以上程序的执行结果为:

java生成随机数代码(Java中生成随机数的4种方式)(8)

③实现原理

通过分析Math的源码我们可以得知:当第一次调用Math.random()方法时,自动创建了一个伪随机数生成器,实际上用的是newjava.util.Random(),当下一次继续调用Math.random()方法时,就会使用这个新的伪随机数生成器。

源码如下:

//创建两个线程for(inti=0;i<2;i++){newThread(()->{//创建Random对象,设置相同的种子Randomrandom=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();}1

总结

本文我们介绍了4种生成随机数的方法,其中Math是对Random的封装,所以二者比较类似。Random生成的是伪随机数,是以当前纳秒时间作为种子数的,并且在多线程竞争比较激烈的情况下因为要进行CAS操作,所以存在一定的性能问题,但对于绝大数应用场景来说,使用Random已经足够了。当在竞争比较激烈的场景下可以使用ThreadLocalRandom来替代Random,但如果对安全性要求比较高的情况下,可以使用SecureRandom来生成随机数,因为SecureRandom会收集一些随机事件来作为随机种子,所以SecureRandom可以看作是生成真正随机数的一个工具类。


发表评论:

最近发表
网站分类
标签列表