1. 程式人生 > >java.util.Random和concurrent.ThreadLocalRandom對比

java.util.Random和concurrent.ThreadLocalRandom對比

  最近工作中遇到了一個需求,需要以一定的概率過濾掉一部分的流量,想想只能用Random了,因為是在多執行緒環境下,我還特意確認了下Random在多執行緒是否能正常執行,Random的實現也比較簡單,初始化的時候用當前的事件來初始化一個隨機數種子,然後每次取值的時候用這個種子與有些MagicNumber運算,並更新種子。最核心的就是這個next的函式,不管你是呼叫了nextDouble還是nextInt還是nextBoolean,Random底層都是調這個next(int bits)。

    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)); }

  為了保證多執行緒下每次生成隨機數都是用的不同,next()得保證seed的更新是原子操作,所以用了AtomicLong的compareAndSet(),該方法底層呼叫了sum.misc.Unsafe的compareAndSwapLong(),也就是大家常聽到的CAS, 這是一個native方法,它能保證原子更新一個數。
  

  既然Random滿足我的需求,又能在多執行緒下正常執行,所以我直接用了random,後來在codeReview中,同事提出用concurrent.ThreadLocalRandom來替代Random。我腦子裡立馬冒出一個問題,既然random是執行緒安全的,為什麼concurrent包裡還要實現一個random。在oracle的jdk文件裡發現這樣一句話

use of ThreadLocalRandom rather than shared Random objects in concurrent programs will typically encounter much less overhead and contention. Use of ThreadLocalRandom is particularly appropriate when multiple tasks (for example, each a ForkJoinTask) use random numbers in parallel in thread pools.

大意就是用ThreadLocalRandom更適合用在多執行緒下,能大幅減少多執行緒並行下的效能開銷和資源爭搶。
  既然文件裡說的牛逼,到底能有多少的效能提升?我做了一個簡單的測試。測試環境:24核 CPU, jdk8,每個隨機生成100000個double數,,分別測試不同執行緒數下rando和ThreadLocalRandom的執行時間,資料如下
這裡寫圖片描述

ThreadNum,Random,ThreadLocalRandom 
50,1192,575
100,4031,162
150,6068,223
200,8093,287
250,10049,248
300,12346,200
350,14429,212
400,16491,62
450,18475,96
500,11311,97
550,12421,90
600,13577,102
650,14718,111
700,15896,127
750,17101,129
800,17907,203
850,19261,226
900,21576,151
950,22206,147
1000,23418,174

  ThreadLocalRandom雖然也有波動,但基本上是平的,而random隨著執行緒數的增加一直在增加,在1000個執行緒時兩者居然有百倍的效能差距。不過這裡有個讓人百思不得其解的現象,為什麼random的耗時在500個執行緒的時候又掉下來,測試多次都是這個情況,可見並不是偶發現象。

  我也在本人的筆記本上測了下,我筆記本雙核i7,ThreadLocalRandom和Random效能差距最高也有100倍,我發現我筆記本比公司伺服器跑的快(資料如下)。。。。我也在一臺1核的阿里雲ECS上測試了,按道理1核心的技術上,即便是多執行緒起始也是序列執行的,但ThreadLocalRandom和Random在1000個執行緒的情況下也有6倍的效能差距。
  這裡寫圖片描述
  既然ThreadLocalRandom在多執行緒下表現這麼牛逼,它究竟是如何做到的?我們來看下原始碼,它的核心程式碼是這個

    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;
    }

  起始ThreadLocalRandom是對每個執行緒都設定了單獨的隨機數種子,這樣就不會發生多執行緒同時更新一個數時產生的資源爭搶了,用空間換時間。
  
最後附上Random和ThreadLocalRandom的效能測試程式碼

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

public class RandomTest {
    private static Random random = new Random();

    private static final int N = 100000;
//    Random from java.util.concurrent.
    private static class TLRandom implements Runnable {
        @Override
        public void run() {
            double x = 0;
            for (int i = 0; i < N; i++) {
                x += ThreadLocalRandom.current().nextDouble();
            }
        }
    }

//    Random from java.util
    private static class URandom implements Runnable {
        @Override
        public void run() {
            double x = 0;
            for (int i = 0; i < N; i++) {
                x += random.nextDouble();
            }
        }
    }

    public static void main(String[] args) {
        System.out.println("threadNum,Random,ThreadLocalRandom");
        for (int threadNum = 50; threadNum <= 2000; threadNum += 50) {
            ExecutorService poolR = Executors.newFixedThreadPool(threadNum);
            long RStartTime = System.currentTimeMillis();
            for (int i = 0; i < threadNum; i++) {
                poolR.execute(new URandom());
            }
            try {
                poolR.shutdown();
                poolR.awaitTermination(100, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String str = "" + threadNum +"," + (System.currentTimeMillis() - RStartTime)+",";

            ExecutorService poolTLR = Executors.newFixedThreadPool(threadNum);
            long TLRStartTime = System.currentTimeMillis();
            for (int i = 0; i < threadNum; i++) {
                poolTLR.execute(new TLRandom());
            }
            try {
                poolTLR.shutdown();
                poolTLR.awaitTermination(100, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(str + (System.currentTimeMillis() - TLRStartTime));
        }
    }
}