1. 程式人生 > 實用技巧 >【Java 併發程式設計系列】【J.U.C】:ThreadlocalRandom

【Java 併發程式設計系列】【J.U.C】:ThreadlocalRandom

Random 類及其侷限性

java.util.Random 是使用較為廣泛的隨機數生成工具類,使用方法如下:

public class RandomTest {
    
    public static void main(String[] args) {

		// 建立一個預設種子的隨機數生成器
		Random random = new Random() ;
		// 輸出10個在0~5(包含0,不包含5)之間的隨機數
		for (int i = 0; i < 10; i++) {
			System.out.println(random.nextInt(5));
		}
	}
}

Random 部分原始碼如下:

public class Random implements java.io.Serializable {

    private final AtomicLong seed; // 種子原子變數
    
    public int nextInt(int bound) {
        if (bound <= 0) // 引數檢查
            throw new IllegalArgumentException(BadBound);

        int r = next(31); // 根據老的種子生成新的種子
        
        ··· // 根據新的種子計算隨機數
        
        return r;
    }
    
    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)); // 使用固定演算法根據新的種子計算隨機數
    }
    
    ···
}

通過閱讀原始碼不難發現,每個Random 例項裡都有一個原子性的種子變數用來記錄當前的種子值,當要生成新的隨機數時需要根據當前種子計算新的種子並更新回原子變數。在多執行緒下使用單個Random 例項生成隨機數時,當多個執行緒同時計算隨機數來計算新的種子時,多個執行緒會競爭同一個原子變數的更新操作,由於原子變數的更新是CAS 操作,同時只有一個執行緒會成功,所以會造成大量執行緒進行自旋重試,這會降低併發效能,所以ThreadLocalRandom 應運而生。

ThreadlocalRandom

為了彌補多執行緒高併發情況下Random 的缺陷, 在JUC 包下新增了ThreadLocalRandom類,使用方法如下:

public class RandomTest {
    
    public static void main(String[] args) {

	// 獲取一個隨機數生成器
	ThreadLocalRandom random2 = ThreadLocalRandom.current();
	// 輸出10個在0~5(包含0,不包含5)之間的隨機數
	for (int i = 0; i < 10; i++) {
	      System.out.println(random.nextInt(5));
	}
    }
}

原始碼分析

Unsafe 機制

private static final sun.misc.Unsafe UNSAFE;
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
static {
    try {
        UNSAFE = sun.misc.Unsafe.getUnsafe(); // 獲取unsafe例項
        Class<?> tk = Thread.class;
        SEED = UNSAFE.objectFieldOffset // 獲取Thread類裡面threadLocalSeed變數在Thread例項裡面的偏移量
            (tk.getDeclaredField("threadLocalRandomSeed"));
        PROBE = UNSAFE.objectFieldOffset // 獲取Thread類裡面threadLocalRandomProbe變數在Thread例項裡面的偏移量
            (tk.getDeclaredField("threadLocalRandomProbe"));
        SECONDARY = UNSAFE.objectFieldOffset // 獲取Thread類裡面threadLocalRandomSecondarySeed變數在Thread例項裡面的偏移量
            (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
    } catch (Exception e) {
        throw new Error(e);
    }
}

ThreadLocalRandom current() 方法

此方法獲取ThreadLocalRandom 例項,並初始化呼叫執行緒中的threadLocalRandomSeed 和threadLocalRandomProbe 變數

static final ThreadLocalRandom instance = new ThreadLocalRandom();

public static ThreadLocalRandom current() {
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0) // 當前執行緒threadLocalRandomProbe變數是否為0,判斷是否第一次呼叫
        localInit(); 
    return instance; // 返回ThreadLocalRandom 例項
}

// 根據probeGenerator計算當前執行緒中的threadLocalRandomProbe初始值,
// 然後根據seeder計算當前執行緒初始種子,並設定到當前執行緒
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);
}

int nextInt(int bound) 方法

計算當前執行緒下一個隨機數

public int nextInt(int bound) {
    if (bound <= 0) // 校驗引數
        throw new IllegalArgumentException(BadBound);
    int r = mix32(nextSeed()); // 根據當前執行緒中的種子計算新種子
    
    ··· // 根據新種子和bound計算隨機數

    return r;
}

// 首先使用r = UNSAFE.getLong(t, SEED) 獲取當前執行緒中threadLocalRandomSeed 變數的值, 
// 然後在種子的基礎上累加GAMMA 值作為新種子,
// 而後使用UNSAFE.putLong 方法把新種子放入當前執行緒的threadLocalRandomSeed 變數中。
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 使用ThreadLocal 的原理,讓每個執行緒都持有一個本地的種子變數,該種子變數只有在使用隨機數時才會被初始化。在多執行緒下計算新種子時是根據自己執行緒內維護的種子變數進行更新,從而避免了競爭。