面試又掛了,這次竟和 Random 有關。。

小強最近面試又翻車了,然而令他鬱悶的是,這次竟然是栽到了自己經常在用的 Random 上......

面試問題

既然已經有了 Random 為什麼還需要 ThreadLocalRandom?

正文

Random 是使用最廣泛的隨機數生成工具了,即使連 Math.random() 的底層也是用 Random 實現的 Math.random() 源碼如下:

面試又掛了,這次竟和 Random 有關。。

可以看出 Math.random() 直接指向了 Random.nextDouble() 方法。


Random 使用

這開始之前,我們先來了解一下 Random 的使用。

<code>Random random = new Random();
for (int i = 0; i < 3; i++) {
// 生成 0-9 的隨機整數
random.nextInt(10);
}/<code>

以上程序的執行結果為:

1

0

7

Random 源碼解析

可以看出 Random 是通過 nextInt() 方法生成隨機整數的,那他的底層的是如何實現的呢?我們來看他的實現源碼:

<code>/**
* 源碼版本:JDK 11
*/
public int nextInt(int bound) {
// 驗證邊界的合法性
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
\t// 根據老種子生成新種子
int r = next(31);
// 計算最大值

int m = bound - 1;
// 根據新種子計算隨機數
if ((bound & m) == 0) // i.e., bound is a power of 2
r = (int)((bound * (long)r) >> 31);
else {
for (int u = r;
u - (r = u % bound) + m < 0;
u = next(31))
;
}
return r;
}/<code>

從以上源碼我們可以看出,整個源碼最核心的部分有兩塊:

  1. 根據老種子生成新種子;
  2. 根據新種子計算出隨機數。

根據新種子計算出隨機數的代碼已經很明確了,我們需要確認一下 next() 方法是如何實現的,繼續看源碼:

<code>/**
* 源碼版本:JDK 11
*/
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)); // 使用 CAS 更新種子

return (int)(nextseed >>> (48 - bits));
}/<code>

根據以上源碼可以看出,在使用老種子去獲取新種子的時候,如果是多線程操作,則同一時刻只會有一個線程 CAS (Conmpare And Swap,比較並交換) 成功,其他失敗的線程會通過自旋等待獲取新種子,因此會有一定的性能消耗

這也是為什麼 JDK 1.7 會引入 ThreadLocalRandom 的答案了,它的出現主要為了提升多線程情況下 Random 的執行效率。那它是如何來提升的?接下來一起來看。

ThreadLocalRandom 使用

我們先來看 ThreadLocalRandom 的類關係圖:

面試又掛了,這次竟和 Random 有關。。

可以看出 ThreadLocalRandom 繼承於 Random 類,先來看它的使用:


<code>ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
for (int i = 0; i < 3; i++) {
// 生成 0-9 的隨機數
System.out.println(threadLocalRandom.nextInt(10));
}/<code>

以上程序的執行結果為:

1

7

5

可以看出 ThreadLocalRandom 和 Random 一樣,都是通過 nextInt() 方法實現隨機整數生成的。

ThreadLocalRandom 源碼解析

接下來我們來看 ThreadLocalRandom 的隨機數是如何生成的,源碼如下:

<code>/**
* 源碼版本:JDK 11
*/
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BAD_BOUND);
// 根據老種子生成新種子
int r = mix32(nextSeed());
int m = bound - 1;
// 根據新種子計算算出隨機數
if ((bound & m) == 0) // power of two
r &= m;

else { // reject over-represented candidates
for (int u = r >>> 1;
u + m - (r = u % bound) < 0;
u = mix32(nextSeed()) >>> 1)
;
}
return r;
}/<code>

從以上源碼可以看出 ThreadLocalRandom 的 nextInt() 和 Random 的 nextInt() 在寫法和實現思路都很像,他們主要的區別在 nextSeed() 方法上,源碼如下:

<code>/**
* 源碼版本:JDK 11
*/
final long nextSeed() {
Thread t; long r; // read and update per-thread seed
// 把當前線程作為參數生成一個新種子
U.putLong(t = Thread.currentThread(), SEED,
r = U.getLong(t, SEED) + GAMMA);
return r;
}
@HotSpotIntrinsicCandidate
public native void putLong(Object o, long offset, long x);/<code>

從以上源碼可以看出,ThreadLocalRandom 並不是像 Thread 那樣使用 CAS 和自旋來獲取新種子,而是在每個線程中使用每個線程中保存自己的老種子來生成新種子,因此就可以避免多線程競爭和自旋等待的時間,所以在多線程環境下性能更高。

ThreadLocalRandom 注意事項

在使用 ThreadLocalRandom 時需要注意一下,在多線程不能共享一個 ThreadLocalRandom 對象,否則會造成生成的隨機數都相同,如下代碼所示:

<code>// 聲明多線程
ExecutorService service = Executors.newCachedThreadPool();
// 共享 ThreadLocalRandom
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
for (int i = 0; i < 10; i++) {
// 多線程執行隨機數並打印結果
service.submit(() -> {
System.out.println(Thread.currentThread().getName() + ":" + threadLocalRandom.nextInt(10));
;
});
}/<code>

以上程序執行結果如下:

pool-1-thread-2:4

pool-1-thread-1:4

pool-1-thread-3:4

pool-1-thread-10:4

pool-1-thread-6:4

pool-1-thread-7:4

pool-1-thread-4:4

pool-1-thread-9:4

pool-1-thread-8:4

pool-1-thread-5:4

Random VS ThreadLocalRandom

Random 生成獲取新種子,如下圖所示:

面試又掛了,這次竟和 Random 有關。。


ThreadLocalRandom 生成獲取新種子,如下圖所示:

面試又掛了,這次竟和 Random 有關。。


性能對比

接下來我們使用 Oracle 官方提供的性能測試工具 JMH (Java Microbenchmark Harness,JAVA 微基準測試套件),來測試一下 Random 和 ThreadLocalRandom 的吞吐量(單位時間內成功執行程序的數量):

<code>import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

/**
* JDK:11
* Windows 10 I5-4460/16G
*/
@BenchmarkMode(Mode.Throughput) // 測試類型:吞吐量
//@Threads(16)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class RandomExample {
public static void main(String[] args) throws RunnerException {
// 啟動基準測試
Options opt = new OptionsBuilder()
.include(RandomExample.class.getSimpleName()) // 要導入的測試類
.warmupIterations(5) // 預熱 5 輪
.measurementIterations(10) // 度量10輪
.forks(1)
.build();
new Runner(opt).run(); // 執行測試
}

/**
* Random 性能測試
*/
@Benchmark
public void randomTest() {
Random random = new Random();
for (int i = 0; i < 10; i++) {
// 生成 0-9 的隨機數
random.nextInt(10);
}
}
/**

* ThreadLocalRandom 性能測試
*/
@Benchmark
public void threadLocalRandomTest() {
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
for (int i = 0; i < 10; i++) {
threadLocalRandom.nextInt(10);
}
}
}/<code>

測試結果如下:

面試又掛了,這次竟和 Random 有關。。

其中,Cnt 表示運行了多少次,Score 表示執行的成績,Units 表示每秒的吞吐量


從 JMH 測試的結果可以看出,ThreadLocalRandom 在併發情況下的吞吐量約是 Random 的 5 倍

完整基準測試代碼下載:github.com/vipstone/bl…

總結

本文講了 Random 和 ThreadLocalRandom 的使用以及源碼分析,Random 是通過 CAS 和自旋的方式生成隨機數,在多線程模式下同一時刻只能有一個線程通過 CAS 獲取到新種子並生成隨機數,其他線程只能自旋等待,所以有一定的性能損耗。而在 JDK 1.7 時新增了 ThreadLocalRandom 它的種子保存在各自的線程中,因此不會有自旋等待的過程,所以高併發情況下性能更優秀。

最後,我們通過官方提供的基準測試工具 JMH 得到的結果,ThreadLocalRandom 的性能大約是 Random 的 5 倍,所以在高併發情況下儘量使用 ThreadLocalRandom。

參考 & 鳴謝 《Java 併發編程之美》翟陸續


作者:王磊的博客
鏈接:https://juejin.im/post/5e72c4ac518825491b11e805




分享到:


相關文章: