發布時間:2024-01-19閱讀(11)
Java中生成隨機數常用的有下面這8種寫法:簡而言之,名稱帶安全的未必安全,名字簡潔的未必簡單。
Math.random()
Talk is cheap, show me the code. 先上源碼:
public static double random() { return RandomnumberGeneratorHolder.randomNumberGenerator.nextDouble();}
private static final class RandomNumberGeneratorHolder { static final Random randomNumberGenerator = new Random();}
從源碼可以看出:Math.random本質上用的就是new Random。而且人家給了一個最佳實踐:使用的時候不用每次就new一個對象,直接做成一個靜態類就可以了。
Random
Random是偽隨機數類,采用線性同余算法產生的。偽隨機數是1946年馮諾依曼提出來的。他在參加一個氫彈的制造項目,需要大量的隨機數模擬核聚變、核裂變。因為當時設備沒辦法去存儲大量的隨機數,所以就想到了這個辦法。偽隨機數(或稱偽亂數)這個稱呼是因為真隨機數都不是計算出來的。這里是使用一個確定性的算法計算出來的似乎是隨機的數序,因此偽隨機數實際上并不隨機。從更宏大的視角看來,是非常有規律的,就像我在《大話高可用》里提到的炊煙。
下面來說說這個線性同余算法,大家不用擔心。我比大家還對算法一竅不通。但是我有人類的常識,這個常識是什么呢?既然是計算得到的,并且人類看起來沒有規律,那一定有一個外部因子(種子),種子是一個變量。順著這個思路看源碼:
/** * 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());}
沒想到在構造函數里就給出了答案,這個因子就是系統時間,精確到納秒。這里為了方便描述,我們舉例想要一個int類型的隨機數(實際它可以產生其他數值類型的隨機數)。來看看源碼:
public int nextInt() { return next(32);}
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));}
解釋一下:調用nextInt默認會生成32個比特位的數字。調用next方法,會有oldseed和nextseed。計算出nextseed會使CAS,將nextseed替換現有oldseed。nextseed的計算方法是oldseed做一些運算,運算的其他數值都是常量。最終返回nextseed值。
因為使用了AtomicLong、自旋 CAS(可以參考《系統梳理一下鎖》),所以Random生成隨機數是線程安全的。new一個全局變量,下次不用新建,Math.random的調用方法是合理的。
通過這個源碼大膽猜測一下:如果兩個不同的進程或者線程,在納秒級相同的時間同時調用new Random,這時候nextInt的返回值是相同的!下次調用nextInt的值也相同!想要驗證的話,可以下載java源碼,在new Random的地方稍作修改,
System.nanoTime()改成固定值,這里我就不驗證了。
ThreadLocalRandom
上面提到Random使用了線程安全的算法:AtomicLong、自旋 CAS。這在保證線程安全的同時造成了很大的性能開銷。ThreadLocalRandom是JDK 7之后提供并發產生隨機數,能夠解決多個線程發生的競爭。
它不是直接用new實例化,而是第一次使用時初始化其靜態方法current()。直接調用靜態方法,可以每個線程實例化一個。有朋友測試過,100個線程的情況下,3分鐘,Math.random可以跑幾百次。而
ThreadLocalRandom.current().nextInt()可以跑十幾萬次!
來看一下它是怎么做到的:
public static ThreadLocalRandom current() { if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0) localInit(); return instance;}
static final void localInit() { int p = probeGenerator.addAndGet(PROBE_INCREMENT); int probe = (p == 0) ? 1 : p; // skip 0 long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT)); Thread t = Thread.currentThread(); UNSAFE.putLong(t, SEED, seed); UNSAFE.putInt(t, PROBE, probe);}
簡而言之,Random是通過系統時間這個外部因子,而ThreadLocalRandom都是通過UNSAFE調用本地方法拿到線程本身的一些變量作為外部因子。所有的參數綁定在線程本身,和其他線程沒有競爭,所以可以不加鎖就保證線程安全。
public int nextInt() { return mix32(nextSeed());}private static int mix32(long z) { z = (z ^ (z >>> 33)) * 0xff51afd7ed558ccdL; return (int)(((z ^ (z >>> 33)) * 0xc4ceb9fe1a85ec53L) >>> 32);}
final long nextSeed() { Thread t; long r; // read and update per-thread seed UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) GAMMA); return r;}
生成隨機數計算的時候也都是調用線程內的變量,不加鎖。大家可以舉一反三一下,回憶一下ThreadLocal相關知識。
SecureRandom
在安全應用場景,隨機數應該使用安全的隨機數。密碼學意義上的安全隨機數,要求必須保證其不可預測性。
由于 Random 是采用時間作為隨機數種子,如果黑客知道隨機數產生的時間,那就能重現隨機數。而 SecureRandom 屬于強隨機數,一般不單獨采用時間作為隨機數種子,除了系統時間,還會采用臨時文件夾中大小、某個線程從休眠到被喚醒所耗的時間等等一系列無法重現的值作為隨機數種子。
因為SecureRandom 采用了很多外部參數,會產生熵源不足時阻塞問題。在我做過的項目中,因為有的業務凌晨沒有流量,這個問題實際發生過。建議有明顯低谷的業務,低谷時低于1TPS時不要用。像SecureRandom這種強隨機在很多場景下效果不一定好。有一個小故事說itunes之前播歌的時候采用的是真隨機。有些用戶總是抱怨說你這是真隨機嗎?怎么來來回回給我播放那幾首歌。蘋果公司有苦說不出,于是把算法改成偽隨機:洗牌算法,先把所有的歌打亂順序,然后按順序播放。改完之后用戶紛紛點贊。
適合使用SecureRandom的場景,比如要給每個資源生成一個url,為了防止url規律被別人攻破,使用爬蟲爬取到自己的資源可以用這個。
UUID.randomUUID()
看下面的代碼就知道UUID.randomUUID()是基于SecureRandom
public static UUID randomUUID() { SecureRandom ng = Holder.numberGenerator; byte[] randomBytes = new byte[16]; ng.nextBytes(randomBytes); randomBytes[6] &= 0x0f; /* clear version */ randomBytes[6] |= 0x40; /* set to version 4 */ randomBytes[8] &= 0x3f; /* clear variant */ randomBytes[8] |= 0x80; /* set to IETF variant */ return new UUID(randomBytes);}
有的朋友說不對呀,網上告訴我UUID是時間戳 時鐘序列 MAC地址。注意上面代碼注釋有個set to version 4。
從UUID的類注釋里就可以看到java 8提到4個版本:
The version field holds a value that describes the type of this {@code* UUID}. There are four different basic types of UUIDs: time-based, DCE* security, name-based, and randomly generated UUIDs. These types have a* version value of 1, 2, 3 and 4, respectively.
翻譯一下:
版本1 - 基于時間
版本2 - 基于DCE sercrity
版本3 - 基于名字(MD5)
版本4 - 基于隨機數
記住啦:java默認UUID是基于隨機數的!
證明一下:如果是基于時間戳的,生成的UUID前幾位應該相同
@Testpublic void test() { for (int i = 0; i < 3; i ) { System.out.println(UUID.randomUUID()); }}
結果:
a0858771-c903-4061-ba6a-53efc372d8a0
17ebdc58-db6c-452f-9b21-b4f54d3958a5
116d6a4f-e50a-4001-b94c-61596da3a75d
事實勝于記憶中的知識,它有可能是錯的!
java UUID也支持版本3:
@Testpublic void test() { for (int i = 0; i < 3; i ) { System.out.println(UUID.nameUUIDFromBytes("編程一生".getBytes())); }}
運行結果:
743f24b2-0914-363b-ace1-f4da750dccad
743f24b2-0914-363b-ace1-f4da750dccad
743f24b2-0914-363b-ace1-f4da750dccad
這個在你的機器上執行也是這個結果,就是MD5了一下。
有人說版本1、版本2怎么用,好像還聽說過版本5。可以用linux命令
uuid -n 3 -v1
運行結果:
5b01cea2-9561-11e9-965b-a3d050dd0f23
5b01cf60-9561-11e9-965c-1b66505f5845
5b01d118-9561-11e9-965d-97354eb9e914
linux命令版本可指定!
RandomStringUtils、RandomUtils和RandomUtil
這兩個不是Java原生的方法,apache commons下有這兩個工具類,本質上還是Random。跟進代碼去看可以發現:RandomUtils用的是JVMRandom。這個只不過是封裝了一個靜態的Random,所有使用RandomUtils類的,全局只用一個,減少了新建對象成本。
如果你問我一般情況下建議用哪個,我選ThreadLocalRandom。那這么好的方法是不是應該也有對應的封裝工具類呢?有的,hultool就搞了一個RandomUtil,
RandomUtil.getRandom()返回的就是ThreadLocalRandom。
RandomUtil.getSecureRandom()返回的是SecureRandom,提供多種選擇。
總結
一張圖表示他們之間的關系:

在《CURD系統怎么做出技術含量--怎樣引導面試》里我提到可以利用工作中總結的技巧驚艷面試官,如果能把一個問題系統性講明白,甚至讓面試官意識到之前的知識竟然是錯的,也一定能讓面試官眼前一亮。
原文鏈接:https://mp.weixin.qq.com/s/9ReYiLCyvJKPXK07fXmrvg
歡迎分享轉載→http://www.avcorse.com/read-32767.html
Copyright ? 2024 有趣生活 All Rights Reserve吉ICP備19000289號-5 TXT地圖HTML地圖XML地圖