Java随机数的原理及优化

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情

本文将介绍Java中的随机方法Random(),为什么它们是伪随机的,以及如何优化使它更加“随机”

什么是伪随机?

伪随机数是看似随机但本质上是固定的周期性序列,即规则随机数

只要这个随机数是确定性算法产生的,就是伪随机数,只能通过不断的算法优化让随机数更接近随机

Math.random()

简介

Java中我们可以通过Math.random()中获得随机数,它返回0~1之间的一个double值

  public static void main(String[] args) {
    double random = Math.random();
    System.out.println("random = " + random);
  }
复制代码

输出:random = 0.10842545820862226

如果你想得到一个int类型的整数,你只需要将上面的结果转换为int类型。 例如,得到0~100之间的int整数。方法如下。

double d = Math.random();
int i = (int) (d*100);
复制代码

随机类

Random 类提供了两个构造函数:

  public Random() {
  }
 
  public Random(long seed) {
  }
复制代码

一个是默认构造函数,一个能够传入随机seed

然后从 Random 对象中获取随机数:

int r = random.nextInt(100);
复制代码

常用API

boolean nextBoolean()    
void  nextBytes(byte[] buf) 
double nextDouble()    
float  nextFloat()     
int   nextInt()    
int   nextInt(int n)   
long  nextLong()      
synchronized double nextGaussian()  //  返回一个正态分布的 double 值,其平均值为0.0标准差为1.0。 
synchronized void setSeed(long seed) // 设置一个Long类型的seed
复制代码

样例


private static void testRandom(Random random) {

    boolean b = random.nextBoolean();
    System.out.println("boolean = " + b);


    byte[] buf = new byte[5];
    random.nextBytes(buf);
    System.out.println("byte = " + Arrays.toString(buf));


    double d = random.nextDouble();
    System.out.println("double = " + d);


    float f = random.nextFloat();
    System.out.println("float = " + f);


    int i0 = random.nextInt();
    System.out.println("没有seed = " + i0);

    int i1 = random.nextInt(100);
    System.out.println("seed = " + i1);

    double gaussian = random.nextGaussian();
    System.out.println("gaussian = " + gaussian);

    long l = random.nextLong();
    System.out.println("long = " + l);
}

public static void main(String[] args) {
    testRandom(new Random());
    System.out.println();
    testRandom(new Random(10));
    System.out.println();
    testRandom(new Random(10));
}
复制代码

执行输出

boolean = true
byte = [116, -117, 27, -44, -71]
double = 0.32145597995841546
float = 0.130032
没有seed = -1309582825
seed = 32
gaussian = -0.5114081918886987
long = 1980360936763293403

boolean = true
byte = [-8, 22, 21, 114, 58]
double = 0.4129126974821382
float = 0.67215943
没有seed = 1048475594
seed = 88
gaussian = 1.1329921492850181
long = -2651111998922877327

boolean = true
byte = [-8, 22, 21, 114, 58]
double = 0.4129126974821382
float = 0.67215943
没有seed = 1048475594
seed = 88
gaussian = 1.1329921492850181
long = -2651111998922877327
复制代码

可以看到,在运行期间,如果seed相同,则随机值相同。

所以同一个seed,产生N个随机数。当你设置seed时,确定了N个随机数。产生相同次数的随机数是相同的。而且如果使用相同的seed创建了两个 Random 实例,则对每个实例应用相同的方法调用序列,它们将生成并返回相同的序列。

原理

我们从 Random 类的构造函数和属性开始:

  private final AtomicLong seed;
 
  private static final long multiplier = 0x5DEECE66DL;
  private static final long addend = 0xBL;
  private static final long mask = (1L << 48) - 1;
 
  private static final double DOUBLE_UNIT = 0x1.0p-53; // 1.0 / (1L << 53)
 
  private static final AtomicLong seedUniquifier
    = new AtomicLong(8682522807148012L);
 
  public Random() {
    this(seedUniquifier() ^ System.nanoTime());
  }
 
  private static long seedUniquifier() {
    for (;;) {
      long current = seedUniquifier.get();
      long next = current * 181783497276652981L;
      if (seedUniquifier.compareAndSet(current, next))
        return next;
    }
  }
 
  public Random(long seed) {
    if (getClass() == Random.class)
      this.seed = new AtomicLong(initialScramble(seed));
    else {
      this.seed = new AtomicLong();
      setSeed(seed);
    }
  }
 
  synchronized public void setSeed(long seed) {
    this.seed.set(initialScramble(seed));
    haveNextNextGaussian = false;
  }
复制代码

有两种构造函数,一种是无参数的,一种是可传递给seed的

seed是第一个用于生成随机数的值。其机制是通过一个函数将seed的值转化为随机数空间中的一个点,得到的随机数均匀地分散在空间中,后续的随机数与第一个随机数相关

seed由seedUniquifier()^System.nanoTime()生成,使用CAS自旋锁实现。使用System.nanoTime()方法,得到一个纳秒时间量,然后连续乘以1817834972766981L直到一次乘法前后结果相同,这是为了增加随机性,这里nanotime可以认为是真正的随机数

但有必要提一下,nanoTime 与我们常用的currenttime方法不同,它返回的是一个随机数,而不是1970-1-1至今的时间

所以不要随意设置随机seed。如果你运行的次数多,你可能会得到相同的随机数。Random生成的seed已经可以满足你平时的需要

Java自带的Random()也容易被破解,攻击者可以通过得到一个一定长度的随机数序列来推断你的seed,然后就可以预测下一个随机数

如何优化随机

主要考虑的是生成的随机数不能重复,如果重复就会重新生成一个。可以使用数组或者Set存储判断是否包含重复的随机数,递归地重新生成一个新的随机数。

猜你喜欢

转载自juejin.im/post/7082404300177014797
今日推荐