zoukankan      html  css  js  c++  java
  • 浅析Java源码之Math.random()

    从零自学java消遣一下,看书有点脑阔疼,不如看看源码!(๑╹◡╹)ノ"""

    ​ JS中Math调用的都是本地方法,底层全是用C++写的,所以完全无法观察实现过程,Java的工具包虽然也有C/C++的介入,不过也有些是自己实现的。

    ​ 本篇文章主要简单阐述Math.random()的实现过程。

    ​ Math隶属于java.lang包中,默认加载。本身是一个final类,方法都是静态方法,所以使用的时候不需要生成一个实例,直接调用Math.XX就行了。

    ​ 一步一步观察该方法,首先是java.lang.Math:

    public final class Math {
      // 大量静态变量与方法
      // ...
      
      private static Random randomNumberGenerator;
    
      private static synchronized void initRNG() {
        if (randomNumberGenerator == null) 
          randomNumberGenerator = new Random();
      }
      
      public static double random() {
        if (randomNumberGenerator == null) initRNG();
        return randomNumberGenerator.nextDouble();
      }
      
      // ...other
    }
    

    ​ 这里面与random相关的操作有3个:

    1、声明一个私有静态Random类randomNumberGenerator

    2、若randomNumberGenerator未初始化,调用new Random()将其初始化

    3、若randomNumberGenerator已经初始化,调用nextDouble方法并将其值返回

    tips:synchronized关键字代表同步执行此方法,Java为多线程,所以为了保证randomNumberGenerator对象只被初始化一次,需要该关键字。比如两个线程同时调用了Math.random(),线程A发现rXX未被初始化,进入initRNG调用new Random()方法。此时线程B也发现了rXX未被初始化,但是initRNG是同步方法,所以挂起等待线程A执行完毕。当线程A执行完后把rXX初始化了,所以在initRNG中的if判断,线程B会直接返回。

    ​ 所以简单来讲,random方法会在第一次调用时生成一个randomNumberGenerator对象,并调用其nextDouble方法生成随机数,之后的调用就只要持续调用此方法返回随机数就行了。

    ​ 下面来看Random类是个什么鬼,来源于java.util.Random:

    public class Random implements java.io.Serializable {
      // 静态变量
      /** use serialVersionUID from JDK 1.1 for interoperability */
      static final long serialVersionUID = 3905348978240129619L;
    
      private final AtomicLong seed;
    
      private final static long multiplier = 0x5DEECE66DL;
      private final static long addend = 0xBL;
      private final static long mask = (1L << 48) - 1;
    
      // constructor
      public Random() { this(++seedUniquifier + System.nanoTime()); }
      private static volatile long seedUniquifier = 8682522807148012L;
    
      public Random(long seed) {
        this.seed = new AtomicLong(0L);
        setSeed(seed);
      }
    
      // 设置种子
      synchronized public void setSeed(long seed) {
        seed = (seed ^ multiplier) & mask;
        this.seed.set(seed);
        haveNextNextGaussian = false;
      }
    
      // 产生大数字
      protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
          oldseed = seed.get();
          nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
      }
    
      // 生成随机数
      public double nextDouble() {
        return (((long)(next(26)) << 27) + next(27))
          / (double)(1L << 53);
      }
    
      // 其他不关心的方法
      // nextBytes(bytes [])
    
      // nextInt
    
      // nextInt(int)
    
      // nextLong
    
      // nextBoolean
    
      // nextFloat
    
      // Serializable相关
    }
    

    ​ 上述代码剔除了大量的注释,还有一些不需要关心的方法,本文只关注Math.random()调用相关方法。

    ​ 对于这个类,首先来看看它的构造函数,理论上new一个Random实例是需要一个long类型的整数作为参数,但是代码用了this使其默认调用new Random(long)这个构造函数。而在构造函数中又生成了一个新类并赋值给实例变量seed,关于这个AtomicLong类其实没啥好讲的,简单看一下就行:

    public class AtomicLong extends Number implements java.io.Serializable {
      private static final long serialVersionUID = 1927816293512124184L;
    
      // valueOffset相关...
    
      // 实例变量
      private volatile long value;
      // 构造函数
      public AtomicLong(long initialValue) {
        value = initialValue;
      }
      public AtomicLong() {}
      // 方法
      public final long get() {
        return value;
      }
      public final void set(long newValue) {
        value = newValue;
      }
      // 这个也会用到 但是不用关心具体实现
      public final boolean compareAndSet(long expect, long update) {
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
      }
      // 其余不需要关心(其实我也看不懂)的方法
    }
    

    ​ 如果思想简单一点,可以看出这个类也很简单,初始化传参赋值,set设置,get获取,多简单!

    ​ 现在回到Random类的构造函数中,实例变量被赋值,类的value为初始化的0(后缀L代表这是一个long类型整数)。下一步调用setSeed,传入构造函数的long类型seed变量(不是seed类),其值为:

    ++seedUniquifier + System.nanoTime()
    // private static volatile long seedUniquifier = 8682522807148012L(8.6825e+15);
    // 2^52 ~ 2^53
    // 写文章时测试 => System.nanoTime() => 13230650355964(1.323e+13);
    

    ​ 其中第一个变量为一个固定值,每次加1,另外一个为System.nanoTime(),该方法返回一个与当前时间相关的数字,具体我不关心。

    ​ 两个相加后,作为初始种子出传入setSeed方法中,方法第一步会对seed进行二次计算:

    seed = (seed ^ multiplier) & mask;
    // private final static long multiplier = 0x5DEECE66DL;(25214903917 => 2.5214e+10)
    // 2^34 ~ 2^35
    // private final static long mask = (1L << 48) - 1;(2^48-1 => 0111...1 => 2^48 = 2.8147+e14)
    

    ​ 此处进行的是位运算,这里不用关心具体数值,只关注可能得到的最大最小值。

    ​ ^ => 异或运算:3 ^ 4 => 011 ^ 100 = 111 => 7(不一样置1,否则置0)

    ​ 可以看出,两个数字异或运算,假设其中较大的二进制位数为n,结果一定是小于等于2n-1,比如34,4为100三位,所以结果一定小于等于2^3-1,即7。

    ​ & => 与运算:3 & 4 => 011 & 100 = 000 => 0(都为1置1,否则置0)

    ​ 可以看出,与运算的结果总是小于等于较小的那个数。

    ​ 这样来再来看之前的位运算:

    seed(2^52 ~ 2^53) ^ multiplier(2^34 ~ 2^35) => 0 ~ (2^53-1)
    
    (seed ^ multiplier)(0 ~ 2^53-1) & mask(2^48-1) => 0 ~ 2^48-1
    

    ​ 结论是种子的范围是在0 ~ 2^48-1之间。

    ​ 测试代码:

    public class test {
      public static void main(String [] args){
        pro b = new pro();
        System.out.println(b.getValue());
        // 256403749474577
        // 256458702577093
        // 256431328421593
      }
    }
    class pro{
      long seed = 8682522807148012L + System.nanoTime();
      long multiplier = 0x5DEECE66DL;
      long mask = (1L << 48) - 1;
      long getValue(){
        return (seed ^ multiplier) & mask;
      }
    }
    

    ​ 构造函数调用完后,现在来看nextDouble,这个方法除去位运算,本质上就是调用了两次next方法:

    public double nextDouble() {
      return (((long)(next(26)) << 27) + next(27))
        / (double)(1L << 53);
    }
    

    ​ 所以直接看next方法:

    protected int next(int bits) {
      long oldseed, nextseed;
      AtomicLong seed = this.seed;
      do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
      } while (!seed.compareAndSet(oldseed, nextseed));
      return (int)(nextseed >>> (48 - bits));
    }
    

    ​ 方法内部声明了2个long类型种子:oldseed、nextseed,通过get方法取得之前位运算得到的seed赋值给oldseed,然后再次通过运算得到一个nextseed的值,并传给seed.compareAndSet(oldseed, nextseed)方法中。

    ​ 关于这个方法,源码里是这样的:

    // java.util.concurrent.atomic.AtomicLong;
    public class AtomicLong extends Number implements java.io.Serializable {
      public final boolean compareAndSet(long expect, long update) {
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
      }
    }
    // sun.misc.Unsafe.java
    public native boolean compareAndSwapLong(Object obj, long offset,long expect, long update);
    

    ​ 这个方法是个内部方法,也就是用C/C++实现的,所以有兴趣的自己去看源码,这里贴一个blog:

    http://www.cnblogs.com/Mainz/p/3546347.html

    ​ 方法的用处简单讲也很简单,比较oldseed与内存中预期的值,如果符合,就将nextseed放进去。

    ​ 这里的运算也不管具体数值,oldseed * multiplier按最大计算会出现溢位,截取成long类型后的大小不确定,所以按照与运算这里的范围依然是0 ~ mask,即0 ~ 2^48-1。

    ​ 最后返回(int)(nextseed >>> (48 - bits)),这里对结果进行类型处理,贴一个类型范围图:

    基本类型 最小值 最大值
    byte -2^7 2^7 - 1
    short -2^15 2^15 - 1
    int -2^31 2^31 - 1
    long -2^63 2^63 - 1

    ​ 若结果是大于int类型最大值,超出的部分会被直接截取砍掉。

    ​ 最后看nextDouble的计算式:

    (((long)(next(26)) << 27) + next(27)) / (double)(1L << 53)
    

    ​ 传入的bits分别为26与27,这时返回的随机数为:

    (int)(nextseed >>> 22) 与 (int)(nextseed >>> 21)
    

    ​ >>>为无符号右移,具体意思就不解释了。

    ​ 得到的结果范围大概是 0 ~ 2^26(27)-1,理论上在这里是不会超过int的最大值。

    ​ 当seed(测试代码中的tmp)为mask时,此时计算会达到最大值:

    (((long)(1L << 53)-1 ) / (double)(1L << 53)
    

    ​ 测试代码:

    public class test {
      public static void main(String [] args){
        testb bb = new testb();
        long a = (long)bb.getNext(26);
        long b = bb.getNext(27);
        double c = 1L << 53;
        double d = ((a<<27) +b)/c;
        // 0.99999999...
        System.out.println(d);
      }
    }
    class testb{
      long tmp = (1L<<48)-1;
      // long tmp = 0 => 0.0
      int getNext(int num){
        return (int)(tmp >>> (48 - num));
      }
    }
    

    ​ 当测试代码中tmp为0时,计算结果为最小值0。

    ​ 每一次调用nextDouble,会生成不一样的seed,也就会返回不一样的数字。

    ​ 这样就是整个随机数生成过程。

    ​ 完结,撒花ヽ(゚∀゚)メ(゚∀゚)ノ

  • 相关阅读:
    Java-抽象类第一篇认识抽象类
    Java-JVM第一篇认识JVM
    ECharts-第一篇最简单的应用
    Oracle 中利用闪回查询确定某表在某时间点之后的修改内容,并恢复至该时间点。
    Oracle 中利用闪回查询确定某表在某时间点之后的修改内容并恢复至该时间点
    Oracle 中利用闪回查询确定某表在某时间点之后的修改内容,并恢复至该时间点
    Oracle 存储过程中的临时表数据自动清空
    oracle闪回,找回已提交修改的记录
    java使字符串的数字加一
    MyEclipse 选中属性或方法后 相同的不变色了?
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/7773589.html
Copyright © 2011-2022 走看看