随机数是骗人的,.Net、Java、C为我作证

随机数是骗人的,.Net、Java、C为我作证

几乎所有编程语言中都提供了"生成一个随机数"的方法,也就是调用这个方法会生成一个数,我们事先也不知道它生成什么数。比如在.Net中编写下面的代码:


  1. Random rand = newRandom();
  2. Console.WriteLine(rand.Next());

运行后结果如下:

 

    Next()方法用来返回一个随机数。同样的代码你执行和我的结果很可能不一样,而且我多次运行的结果也很可能不一样,这就是随机数。

一、陷阱

    看似很简单的东西,使用的时候有陷阱。我编写下面的代码想生成100个随机数:


  1. for (int i=0;i<100;i++)
    {
  2.     Random rand = new Random();
  3.     Console.WriteLine(rand.Next());
  4. }

 

    太奇怪了,竟然生成的"随机数"有好多连续一样的,这算什么"随机数"呀。有人指点"把new Random()"放到for循环外面就可以了:


  1. Random rand = newRandom();
  2. for(int i=0;i<100;i++)
    {
  3. Console.WriteLine(rand.Next());
  4. }

     运行结果:

    确实可以了! 

二、这是为什么呢?

    这要从计算机中"随机数"产生的原理说起了。我们知道,计算机是很严格的,在确定的输入条件下,产生的结果是唯一确定的,不会每次执行的结果不一样。那么怎么样用软件实现产生看似不确定的随机数呢?

    生成随机数的算法有很多种,最简单也是最常用的就是 "线性同余法":  第n+1个数=(第n个数*29+37) % 1000,其中%是"求余数"运算符。很多像我一样的人见了公式都头疼,我用代码解释一下吧,MyRand是一个自定义的生成随机数的类:


  1. class MyRand {
  2.     private int seed;
  3.     public MyRand(int seed)
  4.     {
  5.      this.seed = seed;
  6.     }
  7.     public int Next()
  8.     {
  9.      int next = (seed * 29 + 37) % 1000;
  10.      seed = next;
  11.      return next;
  12.    }
  13. }

 如下调用:


  1. MyRand rand = newMyRand(51);
  2. for (int i = 0; i < 10; i++)  
    {
  3.     Console.WriteLine(rand.Next());
  4. }

执行结果如下:

生成的数据是不是看起来"随机"了。简单解释一下这个代码:我们创建MyRand的一个对象,然后构造函数传递一个数51,这个数被赋值给seed,每次调用Next方法的时候根据(seed * 29 + 37) % 1000计算得到一个随机数,把这个随机数赋值给seed,然后把生成的随机数返回。这样下次再调用Next()的时候seed就不再是51,而是上次生成的随机数了,这样就看起来好像每一次生成的内容都很"随机"了。注意"%1000"取余预算的目的是保证生成的随机数不超过1000。 

当然无论是你运行还是我每次运行,输出结果都是一样的随机数,因为根据给定的初始数据51,我们就可以依次推断下来下面生成的所有"随机数"是什么都可以算出来了。这个初始的数据51就被称为"随机数种子",这一系列的516、1、66、951、616……数字被称为"随机数序列"。我们把51改成52,就会有这样的结果:

三、楼主好人,跪求种子

    那么怎么可以使得每次运行程序的时候都生成不同的"随机数序列"呢?因为我们每次执行程序时候的时间很可能不一样,因此我们可以用当前时间做"随机数种子"


  1. MyRand rand = newMyRand(Environment.TickCount);
  2. for (int i = 0; i < 10; i++)  
    {
  3.     Console.WriteLine(rand.Next());
  4. }

     Environment.TickCount为"系统启动后经过的微秒数"。这样每次程序运行的时候Environment.TickCount都不大可能一样(靠手动谁能一微秒内启动两次程序呢),所以每次生成的随机数就不一样了。

    当然如果我们把new MyRand(Environment.TickCount)放到for循环中: 


  1. for (int i = 0; i < 100; i++)  
    {
  2.     MyRand rand = newMyRand(Environment.TickCount);
  3.     Console.WriteLine(rand.Next());
  4. }

 

    运行结果又变成"很多是连续"的了,原理很简单:由于for循环体执行很快,所以每次循环的时候Environment.TickCount很可能还和上次一样(两行简单的代码运行用不了一毫秒那么长事件),由于这次的"随机数种子"和上次的"随机数种子"一样,这样Next()生成的第一个"随机数"就一样了。从"-320"变成"-856"是因为运行到"-856"的时候时间过了一毫秒。 

四、各语言的实现

    我们看到.Net的Random类有一个int类型参数的构造函数:


  1. public Random(int Seed)

就是和我们写的MyRand一样接受一个"随机数种子"。而我们之前调用的无参构造函数就是给Random(int Seed)传递Environment.TickCount类进行构造的,代码如下:


  1.  public Random() : this(Environment.TickCount)
  2.  {
  3.  }

    这下我们终于明白最开始的疑惑了。  

同样道理,在C/C++中生成10个随机数不应该如下调用:


  1. int i;
  2. for(i=0;i<10;i++)
    {
  3.     srand( (unsigned)time( NULL ) );
  4.     printf("%d\n",rand());
  5. }

 而应该:


  1. srand( (unsigned)time( NULL ) ); //把当前时间设置为"随机数种子"
  2. int i;
  3. for(i=0;i<10;i++)
  4. {
  5. printf("%d\n",rand());
  6. }

 五、"奇葩"的Java

Java学习者可能会提出问题了,在Java低版本中,如下使用会像.Net、C/C++中一样产生相同的随机数: 


  1. for(int i=0;i<100;i++)
  2. {
  3.     Random rand = new Random();
  4.     System.out.println(rand.nextInt());
  5. }

 因为低版本Java中Rand类的无参构造函数的实现同样是用当前时间做种子:


  1. public Random() 
  2. {
  3.   this(System.currentTimeMillis()); 

但是在高版本的Java中,比如Java1.8中,上面的"错误"代码执行却是没问题的:

    为什么呢?我们来看一下这个Random无参构造函数的实现代码:


  1. public Random()
  2. {
  3. this(seedUniquifier() ^ System.nanoTime());
  4. }
  5. private static long seedUniquifier() {
  6. for (;;)
  7. {
  8. long current = seedUniquifier.get();
  9. long next = current * 181783497276652981L;
  10. if (seedUniquifier.compareAndSet(current, next)) return next;
  11. }
  12. }
  13. privatestaticfinal AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);

     这里不再是使用当前时间来做"随机数种子",而是使用System.nanoTime()这个纳秒级的时间量并且和采用原子量AtomicLong根据上次调用构造函数算出来的一个数做异或运算。关于这段代码的解释详细参考这篇文章《解密随机数生成器(2)——从java源码看线性同余算法

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

六、高并发系统中的问题

    前面我们分析了,对于使用系统时间做"随机数种子"的随机数生成器,如果要产生多个随机数,那么一定要共享一个"随机数种子"才会避免生成的随机数短时间之内生成重复的随机数。但是在一些高并发的系统中一个不注意还会产生问题,比如一个网站在服务器端通过下面的方法生成验证码:


  1. Random rand = new Random();
  2. Int code = rand.Next();

    当网站并发量很大的时候,可能一个毫秒内会有很多个人请求验证码,这就会造成这几个人请求到的验证码是重复的,会给系统带来潜在的漏洞。

     再比如我今天看到的一篇文章《当随机不够随机:一个在线扑克游戏的教训》里面就提到了"由于随机数产生器的种子是基于服务器时钟的,黑客们只要将他们的程序与服务器时钟同步就能够将可能出现的乱序减少到只有 200,000 种。到那个时候一旦黑客知道 5 张牌,他就可以实时的对 200,000 种可能的乱序进行快速搜索,找到游戏中的那种。所以一旦黑客知道手中的两张牌和 3 张公用牌,就可以猜出转牌和河牌时会来什么牌,以及其他玩家的牌。"  

    这种情况有如下几种解决方法:

  1. 把Random对象作为一个全局实例(static)来使用。Java中Random是线程安全的(内部进行了加锁处理);.Net中Random不是线程安全的,需要加锁处理。不过加锁会存在会造成处理速度慢的问题。而且由于初始的种子是确定的,所以攻击者存在着根据得到的若干随机数序列推测出"随机数种子"的可能性。
  2. 因为每次生成Guid的值都不样,网上有的文章说可以创建一个Guid计算它的HashCode或者MD5值的方式来做种子: new Random(Guid.NewGuid().GetHashCode()) 。但是我认为Guid的生成算法是确定的,在条件充足的情况下也是可以预测的,这样生成的随机数也有可预测的可能性。当然只是我的猜测,没经过理论的证明。
  3. 采用"真随机数发生器",快看下一节分解!

 七、真随机数发生器

    根据我们之前的分析,我们知道这些所谓的随机数不是真的"随机",只是看起来随机,因此被称为"伪随机算法"。在一些对随机要求高的场合会使用一些物理硬件采集物理噪声、宇宙射线、量子衰变等现实生活中的真正随机的物理参数来产生真正的随机数。

当然也有聪明的人想到了不借助增加"随机数发生器"硬件的方法生成随机数。我们操作计算机时候鼠标的移动、敲击键盘的行为都是不可预测的,外界命令计算机什么时候要执行什么进程、处理什么文件、加载什么数据等也是不可预测的,因此导致的CPU运算速度、硬盘读写行为、内存占用情况的变化也是不可预测的。因此如果采集这些信息来作为随机数种子,那么生成的随机数就是不可预测的了。

在Linux/Unix下可以使用"/dev/random"这个真随机数发生器,它的数据主来来自于硬件中断信息,不过产生随机数的速度比较慢。

Windows下可以调用系统的CryptGenRandom()函数,它主要依据当前进程Id、当前线程Id、系统启动后的TickCount、当前时间、QueryPerformanceCounter返回的高性能计数器值、用户名、计算机名、CPU计数器的值等等来计算。和"/dev/random"一样CryptGenRandom()的生成速度也比较慢,而且消耗比较大的系统资源。

当然.Net下也可以使用RNGCryptoServiceProvider 类(System.Security.Cryptography命名空间下)来生成真随机数,根据StackOverflow上一篇帖子介绍RNGCryptoServiceProvider 并不是对CryptGenRandom()函数的封装,但是和CryptGenRandom()原理类似。  

八、总结

有人可能会问:既然有"/dev/random" 、CryptGenRandom()这样的"真随机数发生器",为什么还要提供、使用伪随机数这样的"假货"?因为前面提到了"/dev/random" 、CryptGenRandom()生成速度慢而且比较消耗性能。在对随机数的不可预测性要求低的场合,使用伪随机数算法即可,因为性能比较高。对于随机数的不可预测性要求高的场合就要使用真随机数发生器,真随机数发生器硬件设备需要考虑成本问题,而"/dev/random"、CryptGenRandom()则性能较差。

万事万物都没有完美的,没有绝对的好,也没有绝对的坏,这才是多元世界美好的地方。 

原文发布时间:2014-05-30

本文来自云栖合作伙伴“linux中国”

时间: 2024-10-30 16:25:32

随机数是骗人的,.Net、Java、C为我作证的相关文章

李开复否认离职:谷歌全球CEO为我作证

会上,李开复否认了近期称其即将离职的传言,并称Eric对媒体做出回答可以证明这一点. 4月27日下午,Google中国总裁李开复与Google全球董事长兼CEO Eric Schmidt出现在Google中国总部,共同会见了国内外媒体.会上,李开复否认了近期称其即将离职的传言,并称Eric对媒体做出回答可以证明这一点. 对于Google过去一年在中国取得的成绩,Eric Schmidt颇为称道.他表示,Google中国在流量.市场份额方面都一直呈现上涨势头,这些都归功于李开复和其带领的中国团队.

java随机数类Random简介

Java实用工具类库中的类java.util.Random提供了产生各种类型随机数的方法.它可以产生int.long.float.double以及Goussian等类型的随机数.这也是它与java.lang.Math中的方法Random()最大的不同之处,后者只产生double型的随机数. 类Random中的方法十分简单,它只有两个构造方法和六个普通方法. 构造方法: (1)public Random() (2)public Random(long seed) Java产生随机数需要有一个基值s

java产生随机数的几种方式

随机数在实际中使用很广泛,比如要随即生成一个固定长度的字符串.数字.或者随即生成一个不定长度的数字.或者进行一个模拟的随机选择等等.Java提供了最基本的工具,可以帮助开发者来实现这一切.     一.Java随机数的产生方式     在Java中,随机数的概念从广义上将,有三种.     1.通过System.currentTimeMillis()来获取一个当前时间毫秒数的long型数字.     2.通过Math.random()返回一个0到1之间的double值.     3.通过Rand

java生成字母数字组合的随机数示例 java生成随机数_java

复制代码 代码如下: package com.test; import java.util.Random; public class GenerateRandomNumber {  public static void main(String[] args) {   System.out.println("生成的10为随机数为:" + getCharAndNumr(10)); }  /**  * java生成随机数字和字母组合  * @param length[生成随机数的长度]  *

Java中生成随机数的实现方法总结_java

在实际开发工作中经常需要用到随机数.如有些系统中创建用户后会给用户一个随机的初始化密码.这个密码由于是随机的,为此往往只有用户自己知道.他们获取了这个随机密码之后,需要马上去系统中更改.这就是利用随机数的原理.总之随机数在日常开发工作中经常用到.而不同的开发语言产生随机数的方法以及技巧各不相同.笔者这里就以Java语言为例,谈谈随机数生成的方法以及一些技巧. 一.利用random方法来生成随机数. 在Java语言中生成随机数相对来说比较简单,因为有一个现成的方法可以使用.在Math类中,Java

JAVA获得包含0-9、a-z、A-Z范围内字符串的的随机数实例_java

一.获得0-9,a-z,A-Z范围的随机字符串 复制代码 代码如下: /** * JAVA获得0-9,a-z,A-Z范围的随机数 * @param length 随机数长度 * @return String */ public static String getRandomChar(int length) { char[] chr = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',

Java中随机数的产生方式与原理详解_java

Java中随机数的产生方式与原理 查阅随机数相关资料,特做整理 首先说一下java中产生随机数的几种方式 在j2se中我们可以使用Math.random()方法来产生一个随机数,这个产生的随机数是0-1之间的一个double,我们可以把他乘以100,他就是个100以内的随机数字,这个在j2me中没有. 在java.util这个包里面提供了一个Random的类,我们可以新建一个Random的对象来产生随机数,他可以生产随机整数.随机float.随机double.随机long,这个也是我们在j2me

Java获取随机数的3种方法_java

主要介绍了Java获取随机数的3种方法,主要利用random()函数来实现 方法1 (数据类型)(最小值+Math.random()*(最大值-最小值+1))例: (int)(1+Math.random()*(10-1+1)) 从1到10的int型随数 方法2 获得随机数 for (int i=0;i<30;i++) {System.out.println((int)(1+Math.random()*10));} (int)(1+Math.random()*10) 通过java.Math包的ra

c++-怎样产生同时两个随机数

问题描述 怎样产生同时两个随机数 我想做两个随机数的乘法,但不知道怎么才能同时产生两个随机数,求各位帮忙 解决方案 没有必要所谓同时产生.你前后产生2个就可以了. 解决方案二: 为什么要同时生成?分两次srand就可以了 解决方案三: c++估计没有这么智能,但是又种软件叫labview的编程语言,可以让2个线程运行在两个核上分别产生随进数,然后用两个线程产生的结果来相乘.本人有段时间不搞c++不知道线程有没有什么法子能让两个线程运行在2个核上 解决方案四: 当你在同一单线程程序里,怎么会同时呢