1. 程式人生 > >隨機數是騙人的,.Net、Java、C為我作證

隨機數是騙人的,.Net、Java、C為我作證

幾乎所有程式語言中都提供了"生成一個隨機數"的方法,也就是呼叫這個方法會生成一個數,我們事先也不知道它生成什麼數。比如在.Net中編寫下面的程式碼:

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

執行後結果如下:

    Next()方法用來返回一個隨機數。同樣的程式碼你執行和我的結果很可能不一樣,而且我多次執行的結果也很可能不一樣,這就是隨機數。

一、陷阱

    看似很簡單的東西,使用的時候有陷阱。我編寫下面的程式碼想生成100個隨機數:

for(inti=0;i<100;i++) { Random rand =
new Random(); 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是一個自定義的生成隨機數的類:

class MyRand { privateint seed; publicMyRand(int seed) { this.seed = seed; } publicint Next() { intnext = (seed * 29 + 37) % 1000; seed = next; returnnext; } }

 如下呼叫:

MyRand rand = newMyRand(51); for (int i = 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 (int i = 0; i < 10; i++) { Console.WriteLine(rand.Next()); }

 Environment.TickCount為"系統啟動後經過的毫秒數"。這樣每次程式執行的時候Environment.TickCount都不大可能一樣(靠手動誰能一毫秒內啟動兩次程式呢),所以每次生成的隨機數就不一樣了。

當然如果我們把new MyRand(Environment.TickCount)放到for迴圈中: 

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

 

    執行結果又變成"很多是連續"的了,原理很簡單:由於for迴圈體執行很快,所以每次迴圈的時候Environment.TickCount很可能還和上次一樣(兩行簡單的程式碼執行用不了一毫秒那麼長事件),由於這次的"隨機數種子"和上次的"隨機數種子"一樣,這樣Next()生成的第一個"隨機數"就一樣了。從"-320"變成"-856"是因為執行到"-856"的時候時間過了一毫秒。 

四、各語言的實現

    我們看到.Net的Random類有一個int型別引數的建構函式:

public Random(int Seed)

就是和我們寫的MyRand一樣接受一個"隨機數種子"。而我們之前呼叫的無參建構函式就是給Random(int Seed)傳遞Environment.TickCount類進行構造的,程式碼如下:

        public Random() : this(Environment.TickCount)
        {
        }

    這下我們終於明白最開始的疑惑了。  

同樣道理,在C/C++中生成10個隨機數不應該如下呼叫:

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

 而應該:

srand( (unsigned)time( NULL ) );//把當前時間設定為"隨機數種子" int i; for(i=0;i<10;i++) {          printf("%d\n",rand()); }

 五、"奇葩"的Java

Java學習者可能會提出問題了,在Java低版本中,如下使用會像.Net、C/C++中一樣產生相同的隨機數: 

?
for(inti=0;i<100;i++) { Random rand =new Random(); System.out.println(rand.nextInt()); }

 因為低版本Java中Rand類的無參建構函式的實現同樣是用當前時間做種子:

public Random() { this(System.currentTimeMillis()); } 

但是在高版本的Java中,比如Java1.8中,上面的"錯誤"程式碼執行卻是沒問題的:

    為什麼呢?我們來看一下這個Random無參建構函式的實現程式碼:

public Random() { this(seedUniquifier() ^ System.nanoTime()); } <br> privatestatic longseedUniquifier() { for (;;) { long current = seedUniquifier.get(); long next = current * 181783497276652981L; if (seedUniquifier.compareAndSet(current, next)) returnnext; } } privatestaticfinal AtomicLong seedUniquifier  =new AtomicLong(8682522807148012L);

     這裡不再是使用當前時間來做"隨機數種子",而是使用System.nanoTime()這個納秒級的時間量並且和採用原子量AtomicLong根據上次呼叫建構函式算出來的一個數做異或運算。關於這段程式碼的解釋詳細參考這篇文章《解密隨機數生成器(2)——從java原始碼看線性同餘演算法

最核心的地方就在於使用static變數AtomicLong來記錄每次呼叫Random建構函式時使用的種子,下次再呼叫Random建構函式的時候避免和上次一樣。

六、高併發系統中的問題

    前面我們分析了,對於使用系統時間做"隨機數種子"的隨機數生成器,如果要產生多個隨機數,那麼一定要共享一個"隨機數種子"才會避免生成的隨機數短時間之內生成重複的隨機數。但是在一些高併發的系統中一個不注意還會產生問題,比如一個網站在伺服器端通過下面的方法生成驗證碼:

Random rand = new Random();

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()則效能較差。

萬事萬物都沒有完美的,沒有絕對的好,也沒有絕對的壞,這才是多元世界美好的地方。

如鵬網.Net培訓班正在報名,有網路的地方就可以參加如鵬網的學習,學完就能高薪就業,點選此處瞭解

三年前只要懂“三層架構”就可以說“精通分層架構”;現在則需要懂IOC(AutoFac等)、CodeFirst、lambda、DTO等才值錢;

三年前只要會SQLServer就可以說自己“精通資料庫開發”;現在則需還需要掌握MySQL等開源資料庫才能說是“.Net開源”時代的程式設計師;

三年前只要會進行使用者上傳內容的安全性處理即可;現在則需要熟悉雲端儲存、CDN等才能在雲端計算時代遊刃有餘;

三年前只要掌握Lucene.Net就會說自己“熟悉站內搜尋引擎開發”;現在大家都用ElasticSearch了,你還用Lucene.Net就太老土了;

三年前發郵件還是用SmtpClient;現在做大型網站發郵件必須用雲郵件引擎;

三年前快取就是Context.Cache;現在則是Redis、Memcached的天下;

如鵬網再次引領.Net社群技術潮流!點選此處瞭解如鵬網.Net最新課程


相關推薦

隨機數騙人.NetJavaC作證

幾乎所有程式語言中都提供了"生成一個隨機數"的方法,也就是呼叫這個方法會生成一個數,我們事先也不知道它生成什麼數。比如在.Net中編寫下面的程式碼: Random rand = newRandom(); Console.WriteLine(rand.Next());

Unix時間戳轉日期時間格式C#JavaPython各語言實現!

[1] oda total 轉換 n) str nbsp -m col 之前有個Q上好友沒事問我,怎麽自己寫Unix時間戳轉日期時間?於是我就順手寫了個C#版本給他!最近想起來,就萌發多寫幾個語言的版本分享,權當練習思路外加熟悉另外兩種語言。 先說轉換步驟 先處理年份

比較分析C++JavaPythonR語言的面向物件特徵這些特徵如何實現的?有什麼相同點?

一門課的課後題答案,在這裡備份一下: 面向物件程式設計語言 –  比較分析C++、Java、Python、R語言的面向物件特徵,這些特徵如何實現的?有什麼相同點? C++ 語言的面向物件特徵: 物件模型:封裝 (1)  訪問控制機制: C++提供完善的訪問控制機制,分別是: p

程式設計科普CC++JavaPHPPython分別用來開發什麼?

首先,我們先普及一下程式語言的基礎知識。用任何程式語言來開發程式,都是為了讓計算機幹活。比如編寫一篇文章,下載一首MP3等。 而計算機幹活的CPU只認識機器的指令。所以,儘管不同的程式語言差異極大,最後都得“翻譯”成CPU可以執行的機器指令。 理論上任何語言幹任

VC/C++HTMLJavaSQLJSPSSHSSM等等的英語單詞(看了很多遍都會忘記意思或讀音只知道大概用來幹嘛的)

C++、HTML、Java、SQL、JSP、SSH、SSM等等學習過程中遇到的英語單詞(看了很多遍都會忘記意思或讀音,只知道大概用來幹嘛的) 在這裡記錄下來,方便檢視(有道詞典的死粉),順序A~Z(首字母大寫的單詞也是)【我找到的"程式設計開發常用的英語單詞"文章分享給大家

Python 駕乘AI之力碾壓JavaC++等老牌語言?

  學習Python中有不明白推薦加入交流裙                 號:735934841              

CC++JavaJavaScriptPHPPython有啥區別終於搞清楚了

tst 平臺 暴雪 一個 巨無霸 web瀏覽器 elastic 高效 開發 c語言主要用於底層和驅動,編譯器開發 c語言高效底層,主要用於底層的開發,比如一些系統驅動;unix,linux系統和其上相關應用的開發; 有很多語言比如python、php、perl、ruby

一文了解c/c++javaJavaScriptphpPython的用途

BE 算法實現 分布式 底層 web服務器 物聯網 文檔 性能 爆發 編程語言是用來定義計算機程序的形式語言。它是一種被標準化的交流技巧,用來向計算機發出指令。一種計算機語言讓程序員能夠準確地定義計算機所需要使用的數據,並精確地定義在不同情況下所應當采取的行動。 C和C+

Notepad++一鍵編譯運行(PythonJavaC++)

nbsp cnblogs anaconda not ont tps -o d+ 命令 Python 需要事先安裝Python配置好環境變量。建議使用Anaconda,方便。 在Notepad按F5,輸入如下   cmd /k chdir /d $(CURRENT_DI

2018年慕課網視頻教程(vuereactdockerpythonjavaGo語言)

java、 tom 深度 面試 銷售 拉勾網 react code python升級 如需下述哪一個課程,加QQ: 3475362830,非免費,幾大洋,非誠勿擾! Go語言實戰流媒體視頻網站基於Golang協程實現流量統計系統Google資深工程師深度講解Go語言 jav

免費視訊教程2018最新JavaPYthonweb視訊

前面分享了C語言基礎後,學了c語言自然要學一門面向物件的高階語言,今天給大家分享的語言就是java, 直接一部到位,java初級-》中級-》高階 本課程是Java語言的經典課程,採用最著名的教材《Java程式設計思想》,《Java程式設計思想》包含的內容非常多,我們把這本書分成初級教程、中級

CC++JavaJavaScriptPHPPythonRuby 這些語言分別主要用來開發什麼?

pansz,歡迎評論 此貼純科普用,以下僅僅說主要用途,其他用途限於篇幅關係省略之,不要鑽牛角尖。 C:系統底層,驅動程式,嵌入式底層,基礎服務程式。 C++:上層服務程式,應用API,大型3D遊戲。 Java:服務端應用程式,以及客戶端應用程式。 JS:在瀏覽器中執行的程式。 PHP:Web伺

CC++JavaJavaScriptPHPPythonRuby這些語言分別主要用來開發什麼?

c語言主要用於底層和驅動,編譯器開發 c語言高效底層,主要用於底層的開發,比如一些系統驅動;unix,linux系統和其上相關應用的開發; 有很多語言比如python、php、perl、ruby等都c語言開發的這些語言的核心庫,編譯器等開發都是c語言。比如鳥哥的php7的開發,就基

通過dos命令方式運用javacjavajar實現對Java檔案的編譯執行及打包的完整過程

環境配置一定要配好!!! 編寫一個簡單的HelloWorld.java: package com.bnusri; public class HelloWorld { public static void main(String[] args) { // TODO

正則表示式總結(JavaScriptpythonPHPgolangC++javaC#)之彙總

第一種:JavaScript(ES6) A:分隔符: 例如:reg=/\d(.)\1/g  (切記:不能帶單引號和雙引號) X:ES6之新規則:s 修飾符:dotAll 模式 正則表示式中,點(.)是一個特殊字元,代表任意的單個字元,但是行終止符(line terminat

基於GSM MODEM簡訊貓二次開發說明和免費二次開發包提供C#VCDelphilinxJAVAPB等語言源程式

隨著電子資訊的不斷髮展,資訊科技的高速發展和廣泛應用中,簡訊技術也是資訊科技的前沿和熱點.許多系統管理軟體和辦公軟體在監控到資料異常時無法及時告知管理人員,使系統管理軟體發揮作用降低。而採用簡訊模組與系統管理軟體相結合進行有效實時監控,使在出現故障和異常情況時自動傳送報警資訊

PHPJavaPythonCC++ 這幾種程式語言都各有什麼特點或優點?

Java 猶如宮澤賢治的《不畏風雨》中出現的、性格木訥的女孩子。從小就由於遲鈍和大食量等特徵被別人當作笨蛋,從小學入學開始進入田徑部、堅持跑步,在中長跑中經常取得好成績,給人以活潑的印象。是十分努力的女孩子。 她的家境並不算好。父親Sun是有才能的藝術家,但不擅長理財,在她14歲的時候因為苦於借債積勞

CC++JavaC#運算子優先順序

轉自:https://baike.baidu.com/item/%E8%BF%90%E7%AE%97%E7%AC%A6%E4%BC%98%E5%85%88%E7%BA%A7/4752611?fr=aladdin C語言優先順序 優先順序 運算子

PHPJAVAC#Object-CAndroid 通用的DES加密解密

PHP加密解密: class JoDES { private static $_instance = NULL; /** * @return JoDES */ public static function sh