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

Random rand = newRandom();

Console.WriteLine(rand.Next());

运行后结果如下:

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

一、陷阱

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

for(inti=0;i<100;i++)

{

Random rand = newRandom();

Console.WriteLine(rand.Next());

}

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

Random rand = newRandom();

for(inti=0;i<100;i++)

{

Console.WriteLine(rand.Next());

}

运行结果:

确实可以了!

二、这是为什么呢?

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

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

classMyRand

{

privateintseed;

publicMyRand(intseed)

{

this.seed = seed;

}

publicintNext()

{

intnext = (seed * 29 + 37) % 1000;

seed = next;

returnnext;

}

}

如下调用:

MyRand rand = newMyRand(51);

for(inti = 0; i < 10; i++)

{

Console.WriteLine(rand.Next());

}

执行结果如下:

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

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

三、为什么每次生成的随机数都一样(楼主好人,跪求种子)

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

MyRand rand = newMyRand(Environment.TickCount);

for(inti = 0; i < 10; i++)

{

Console.WriteLine(rand.Next());

}

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

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

for(inti = 0; i < 100; i++)

{

MyRand rand = newMyRand(Environment.TickCount);

Console.WriteLine(rand.Next());

}

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

四、各语言的实现

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

1publicRandom(intSeed)

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

1publicRandom() : this(Environment.TickCount){}

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

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

inti;

for(i=0;i<10;i++)

{

srand( (unsigned)time( NULL ) );

printf("%dn",rand());

}

而应该:

srand( (unsigned)time( NULL ) ); //把当前时间设置为"<a class="highlight" id="highlight"><a class="highlight highlight" id="highlight" style="undefined">随机数</a></a>种子"

inti;

for(i=0;i<10;i++)

{

printf("%dn",rand());

}

五、"奇葩"的Java

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

for(inti=0;i<100;i++)

{

Random rand = newRandom();

System.out.println(rand.nextInt());

}

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

1publicRandom() { this(System.currentTimeMillis()); }

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

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

publicRandom()

{

this(seedUniquifier() ^ System.nanoTime());

}

privatestaticlongseedUniquifier() {

for(;;) {

longcurrent = seedUniquifier.get();

longnext = current * 181783497276652981L;

if(seedUniquifier.compareAndSet(current, next))

returnnext;

}

}

1privatestaticfinal AtomicLong seedUniquifier  = newAtomicLong(8682522807148012L);

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

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

六、高并发系统中的问题

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

Random rand = newRandom();

Int code = rand.Next();

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

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

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

把Random对象作为一个全局实例(static)来使用。Java中Random是线程安全的(内部进行了加锁处理);.Net中Random不是线程安全的,需要加锁处理。不过加锁会存在会造成处理速度慢的问题。而且由于初始的种子是确定的,所以***者存在着根据得到的若干随机数序列推测出"随机数种子"的可能性。

因为每次生成Guid的值都不样,网上有的文章说可以创建一个Guid计算它的HashCode或者MD5值的方式来做种子: new Random(Guid.NewGuid().GetHashCode()) 。但是我认为Guid的生成算法是确定的,在条件充足的情况下也是可以预测的,这样生成的随机数也有可预测的可能性。当然只是我的猜测,没经过理论的证明。

七、如何生成真随机数,真随机数发生器

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

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

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

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

当然.Net下也可以使用RNGCryptoServiceProvider 类(System.Security.Cryptography命名空间下)来生成真随机数,根据StackOverflow上一篇帖子介绍

RNGCryptoServiceProvider 并不是对CryptGenRandom()函数的封装,但是和CryptGenRandom()原理类似。

publicstaticintRealRandom(intseed = Int32.MaxValue)

{

byte[] b = newbyte[4];

newRNGCryptoServiceProvider().GetBytes(b);

returnnewRandom(BitConverter.ToInt32(b, 0)).Next(seed);

}

八、总结

有人可能会问:既然有"/dev/random" 、CryptGenRandom()这样的"真随机数发生器",为什么还要提供、使用伪随机数这样的"假货"?

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

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

转载于:https://blog.51cto.com/xqtesting/2405602

所有编程语言为我作证,随机数骗局大揭秘,随机数都是骗人的!相关推荐

  1. 比特协议是骗局吗_山寨币也玩收割,“BTG比特黄金”挖矿骗局大揭秘

    2021开年第一热搜,毫无疑问被比特币给预定了!暴涨的行情让整个市场情绪攀升到了高潮,与此同时,炒币,虚拟数字货币,区块链,币市等一些名词就像魔术一样,吸引着众多人的眼球,让人在心生羡慕的同时防不胜防 ...

  2. 随机数是骗人的,.Net、Java、C为我作证 - 杨中科 - 博客园

    移动端全链路跟踪保障体系 随机数是骗人的,.Net.Java.C为我作证 - 杨中科 - 博客园

  3. 谷歌钦定的编程语言Kotlin大揭秘

    第一时间关注程序猿(媛)身边的故事 谷歌钦定的编程语言Kotlin大揭秘 语法+高级特性+实现原理:移动开发者升职加薪宝典! 谷歌作为世界级的科技公司巨头,强悍的技术研发与创新能力使其一直是业界的楷模 ...

  4. Android 9(P)应用进程创建流程大揭秘

           Android 9 (P)应用进程创建流程大揭秘 Android 9 (P)系统启动及进程创建源码分析目录: Android 9 (P)之init进程启动源码分析指南之一 Android ...

  5. 黑客爱用的 HOOK 技术大揭秘!

    黑客爱用的 HOOK 技术大揭秘! 什么是 HOOK 技术? 病毒木马为何惨遭杀软拦截? 商业软件为何频遭免费破解? 系统漏洞为何能被补丁修复? 这一切的背后到底是人性的扭曲,还是道德的沦丧,敬请收看 ...

  6. 360黑匣子之谜——奇虎360“癌”性基因大揭秘

    360黑匣子之谜--奇虎360"癌"性基因大揭秘 http://www.nbd.com.cn 2013-02-26 01:33 核心提示: 360怎么了?这是一家什么样的企业?带着 ...

  7. 康复治疗学可以考计算机吗,【大揭秘】2018“人机对话”康复医学治疗技术专业技术资格考试...

    原标题:[大揭秘]2018"人机对话"康复医学治疗技术专业技术资格考试 昨天,关于"2018年康复医学治疗技术专业技术资格考试采用人机对话考试方式"的通知一经发 ...

  8. 男人约会动机大揭秘。

    被人约是好事,但也要眼明心亮,男人约会动机大揭秘. 男人约会动机大揭秘: 1:这是一个有诚意的开始,表明他是早有计划和你约会的.至少,也说明他有相当的社交礼貌. 2:这个男人可能只是一时寂寞了,如果你 ...

  9. 诗人般的机器学习,ML工作原理大揭秘

    诗人般的机器学习,ML工作原理大揭秘 https://www.cnblogs.com/DicksonJYL/p/9698208.html 选自arXiv 作者:Cassie Kozyrkov 机器之心 ...

  10. python就业方向及工资-【行情分享】python就业方向与薪资大揭秘

    原标题:[行情分享]python就业方向与薪资大揭秘 学python,我们要首先问自己,是为了转行?提升自己?还是什么,有了明确的目标,才会沉下心来学习.我学习python的目标是想要转行,可以跟大家 ...

最新文章

  1. linux share目录 多个机器访问,如何在linux下网上邻居访问另一台机windows共享的目录?(急)...
  2. haproxy + keepalived 安装配置
  3. Tinyhttpd的实现和一些基本问题的解决
  4. ISO13485 相关
  5. protobuf入门教程(五):枚举(enum)、包(package)
  6. 【Java设计模式】工厂模式
  7. python3.8文档_python 3.8的新功能
  8. vs下使用qt设置应用程序的图标
  9. Transformer、BERT细节基础知识点
  10. lnmp安装完之后的一些注意事项
  11. 关于动态规划的一点学习感想
  12. modbus协议的常用测试工具
  13. python人脸对比相似度_相似度算法原理及python实现
  14. wps永久关闭热点功能
  15. Selenium_python 断言
  16. 电视制式以及伴音载频制式详解
  17. CT一般扫描参数_头颅CT扫描技术
  18. ipmitool 命令行重启服务器
  19. 详解冬奥冠军背后的AI黑科技
  20. 【BZOJ3622】已经没有什么好害怕的了

热门文章

  1. [題解]luogu_P2055假期的宿舍(二分圖最大匹配)
  2. TCP header
  3. org.apache.jasper.JasperException: /index.jsp(14,2) The s:form tag declares that it accepts dyna
  4. Clojure 的 Enlive 库尝试
  5. 《那些年啊,那些事——一个程序员的奋斗史》——06
  6. 数据结构 关键路径的个人理解
  7. 读吴恩达算-EM算法笔记
  8. JavaScript封装缓动动画函数
  9. spring中afterPropertiesSet方法与init-method配置描述
  10. Javascript 535种方式!!!实现页面重载