关闭

解密随机数生成器

标签: javarandom算法
159人阅读 评论(0) 收藏 举报

前短时间写了个公司年会抽奖的程序,不少人反应有的人连续几年中奖,于是怀疑程序的问题,今天特地抽出点时间翻看了下java的源码,解读下随机函数的生成算法。

在Eclipse中输入java.util.Random,按F3转到Random类的源代码:

首先,我们看到这样一段说明:

/**
 * An instance of this class is used to generate a stream of
 * pseudorandom numbers. The class uses a 48-bit seed, which is
 * modified using a linear congruential formula. (See Donald Knuth,
 * <i>The Art of Computer Programming, Volume 2</i>, Section 3.2.1.)
 * <p>


翻译过来是:

这个类的一个实现是用来生成一串伪随机数。这个类用了一个48位的种子,被线性同余公式修改用来生成随机数。

显然,java的Random类使用的是线性同余法来得到随机数的。

接着往下看,我们找到了它的构造函数与几个方法,里面包含了获得48位种子的过程:


/**
     * Creates a new random number generator. This constructor sets
     * the seed of the random number generator to a value very likely
     * to be distinct from any other invocation of this constructor.
     */
    public Random() {
        this(seedUniquifier() ^ System.nanoTime());
    }

    private static long seedUniquifier() {
        // L'Ecuyer, "Tables of Linear Congruential Generators of
        // Different Sizes and Good Lattice Structure", 1999
        for (;;) {
            long current = seedUniquifier.get();
            long next = current * 181783497276652981L;
            if (seedUniquifier.compareAndSet(current, next))
                return next;
        }
    }

    private static final AtomicLong seedUniquifier
        = new AtomicLong(8682522807148012L);

   public Random(long seed) {
        if (getClass() == Random.class)
            this.seed = new AtomicLong(initialScramble(seed));
        else {
            // subclass might have overriden setSeed
            this.seed = new AtomicLong();
            setSeed(seed);
        }
    }

这里先使用System.nanoTime()获取一个纳秒级的时间量,参与48位种子的构成,这里使用了System.nanoTime()方法来得到一个纳秒级的时间量,参与48位种子的构成,然后还进行了一个很变态的运算——不断乘以181783497276652981L,直到某一次相乘前后结果相同——来进一步增大随机性。、

好了,现在我不得不佩服这位工程师的变态了:到目前为止,这个程序已经至少进行了三次随机:

 

1、获得一个长整形数作为“初始种子”(系统默认的是8682522807148012L)

 

2、不断与一个变态的数——181783497276652981L相乘(天知道这些数是不是工程师随便滚键盘滚出来的-.-)直到某一次相乘前后数值相等

 

3、与系统随机出来的nanotime值作异或运算,得到最终的种子

接下来利用了线性同余算法

 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));
    }

    线性同余法是一个很古老的随机数生成算法,它的数学形式如下:

Xn+1 = (a*Xn+c)(mod m) 

其中,

m>0,0<a<m,0<c<m

 

这里Xn这个序列生成一系列的随机数,X0是种子。随机数产生的质量与m,a,c三个参数的选取有很大关系。这些随机数并不是真正的随机,而是满足在某一周期内随机分布,这个周期的最长为m。根据Hull-Dobell Theorem,当且仅当:

1. c和m互素;

 

2. a-1可被所有m的质因数整除;

 

3. 当m是4的整数倍,a-1也是4的整数倍

 

时,周期为m。所以m一般都设置的很大,以延长周期。



 private static long initialScramble(long seed) {
        return (seed ^ multiplier) & mask;
    


    private static final long multiplier = 0x5DEECE66DL;
    private static final long addend = 0xBL;
    private static final long mask = (1L << 48) - 1;



对比数学公式,a = 0x5DEECE66DL, c = 0xBL, m = 2^48,就可以得到一个48位的随机数,而且这个谨慎的工程师进行了迭代,增加结果的随机性。再把结果移位,就可以得到指定位数的随机数。



随机算法中,最核心的地方就在于使用static变量AtomicLong来记录每次调用Random构造函数时使用的种子,下次再调用Random构造函数的时候避免和上次一样。


这里有点晦涩难懂,不多说了,请参见解密随机数生成器(2)——从java源码看线性同余算法



最后将随机数打印出来:0.47730637287039723
0.8024508793535536
0.5481799061191626
0.27565830804489333
0.047160003035978115
0.7035259724009703
0.6550010669273417
0.6482388034909883
0.0576209291980867
0.07069050337047089
0.9336952730245858
0.13472579417197172
0.10999371780411427
0.38668647801543354
0.994986602594389
0.37053761228696236
0.4269821582971406
0.30965439072331447
0.26730127990451324
0.15780248921270124
0.4874277631424623
0.43866923824434134
0.8379735008094493
0.46585350668133274
0.03358481257929935
0.8894735657161117
0.46912683775771524
0.9431080765561418
0.9167830882120481
0.17286940293254904
0.19988344534144653

... ...


其结果是[0,1) 中的随机分布值





   
0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:215次
    • 积分:13
    • 等级:
    • 排名:千里之外
    • 原创:0篇
    • 转载:1篇
    • 译文:1篇
    • 评论:0条
    文章存档