10、JDK 17 新特性:增强的伪随机数生成器实战:多线程环境下的随机数生成

多线程环境下生成随机数,鹏磊最烦的就是线程安全问题。用 Random 得加锁,性能差;用 ThreadLocalRandom 功能有限;用 SplittableRandom 又不知道怎么用。JDK 17 的增强伪随机数生成器终于解决了这些问题,提供了多种多线程安全的方案,性能也好。

多线程环境下生成随机数是个常见需求,但传统方式要么性能差,要么功能有限。增强的伪随机数生成器提供了 SplittableRandomThreadLocalRandom 两种方案,还有并行流支持,让你能根据需求选择合适的方案。这玩意儿对于并行计算、模拟、游戏这些需要大量随机数的场景特别有用。

多线程随机数生成的问题

多线程环境下生成随机数,有几个常见问题:

问题一:线程安全

传统 Random 不是线程安全的,多线程使用需要同步:

import java.util.*;

// 错误:多线程使用 Random 不安全
Random random = new Random();  // 共享的 Random

// 多个线程同时使用会出问题
new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        int value = random.nextInt();  // 可能产生竞争条件
    }
}).start();

new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        int value = random.nextInt();  // 可能产生竞争条件
    }
}).start();

多线程使用 Random 不安全,可能产生竞争条件,结果也不可预测。

问题二:性能瓶颈

加锁同步会导致性能瓶颈:

import java.util.*;

// 加锁同步,性能差
Random random = new Random();  // 共享的 Random
Object lock = new Object();  // 锁对象

// 多个线程竞争锁,性能差
new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        synchronized (lock) {
            int value = random.nextInt();  // 需要加锁
        }
    }
}).start();

加锁同步会导致线程阻塞,性能差。

SplittableRandom:可分割的生成器

SplittableRandom 是可分割的生成器,适合并行计算:

import java.util.*;

// 创建 SplittableRandom
SplittableRandom random = new SplittableRandom();  // 主生成器

// 分割生成器(用于并行计算)
SplittableRandom random1 = random.split();  // 子生成器 1
SplittableRandom random2 = random.split();  // 子生成器 2
SplittableRandom random3 = random.split();  // 子生成器 3

// 在不同线程使用独立的生成器
new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        int value = random1.nextInt(100);  // 使用子生成器 1
        // 处理随机数
    }
}).start();

new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        int value = random2.nextInt(100);  // 使用子生成器 2
        // 处理随机数
    }
}).start();

new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        int value = random3.nextInt(100);  // 使用子生成器 3
        // 处理随机数
    }
}).start();

SplittableRandom 可分割,每个线程用独立的生成器,不需要同步,性能好。

并行流中的随机数生成

并行流是 SplittableRandom 的典型应用:

import java.util.*;
import java.util.stream.*;

// 并行流中使用 SplittableRandom
public class ParallelRandomExample {
    public static void main(String[] args) {
        SplittableRandom random = new SplittableRandom();  // 创建生成器
        
        // 并行生成随机数
        List<Integer> randomNumbers = random.ints(10_000, 0, 100)  // 生成 10000 个 0-99 的随机整数
            .parallel()  // 并行处理
            .boxed()  // 装箱
            .collect(Collectors.toList());  // 收集为列表
        
        System.out.println("生成随机数数量: " + randomNumbers.size());  // 输出数量
        
        // 并行处理随机数
        long count = randomNumbers.parallelStream()  // 并行流
            .filter(n -> n > 50)  // 过滤大于 50 的数
            .count();  // 统计数量
        
        System.out.println("大于 50 的数量: " + count);  // 输出数量
    }
}

并行流用 SplittableRandom,每个线程用独立的生成器,性能好,结果也正确。

实际应用:并行蒙特卡洛模拟

并行蒙特卡洛模拟是 SplittableRandom 的典型应用:

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;

// 并行蒙特卡洛模拟:计算 π
public class ParallelMonteCarloPi {
    private final SplittableRandom random;  // 随机数生成器
    
    public ParallelMonteCarloPi() {
        this.random = new SplittableRandom();  // 创建生成器
    }
    
    // 并行计算 π
    public double estimatePi(long iterations, int threads) {
        // 创建线程池
        ForkJoinPool pool = new ForkJoinPool(threads);  // 创建线程池
        
        try {
            // 并行计算
            long insideCircle = pool.submit(() -> {
                // 为每个线程创建独立的生成器
                return LongStream.range(0, iterations)  // 创建范围流
                    .parallel()  // 并行处理
                    .mapToLong(i -> {
                        // 每个线程创建独立的生成器
                        SplittableRandom localRandom = random.split();  // 分割生成器
                        double x = localRandom.nextDouble();  // 随机 X 坐标
                        double y = localRandom.nextDouble();  // 随机 Y 坐标
                        return (x * x + y * y <= 1.0) ? 1 : 0;  // 判断是否在圆内
                    })
                    .sum();  // 统计在圆内的点数
            }).get();  // 获取结果
            
            return 4.0 * insideCircle / iterations;  // 计算 π 的估计值
        } catch (Exception e) {
            throw new RuntimeException(e);  // 抛异常
        } finally {
            pool.shutdown();  // 关闭线程池
        }
    }
}

// 使用示例
ParallelMonteCarloPi mc = new ParallelMonteCarloPi();  // 创建模拟器
double pi = mc.estimatePi(10_000_000, 4);  // 使用 1000 万次迭代,4 个线程
System.out.println("π 的估计值: " + pi);  // 输出估计值

并行蒙特卡洛模拟用 SplittableRandom,每个线程用独立的生成器,性能好,结果也正确。

实际应用:并行数据生成

并行数据生成也是 SplittableRandom 的典型应用:

import java.util.*;
import java.util.stream.*;
import java.util.concurrent.*;

// 并行数据生成器
public class ParallelDataGenerator {
    private final SplittableRandom random;  // 随机数生成器
    
    public ParallelDataGenerator() {
        this.random = new SplittableRandom();  // 创建生成器
    }
    
    // 并行生成测试数据
    public List<TestData> generateTestData(int count, int threads) {
        ForkJoinPool pool = new ForkJoinPool(threads);  // 创建线程池
        
        try {
            return pool.submit(() -> {
                // 为每个线程创建独立的生成器
                return IntStream.range(0, count)  // 创建范围流
                    .parallel()  // 并行处理
                    .mapToObj(i -> {
                        // 每个线程创建独立的生成器
                        SplittableRandom localRandom = random.split();  // 分割生成器
                        
                        // 生成测试数据
                        int id = localRandom.nextInt(1_000_000);  // 随机 ID
                        String name = "User" + localRandom.nextInt(10000);  // 随机名称
                        double score = localRandom.nextDouble(0.0, 100.0);  // 随机分数
                        
                        return new TestData(id, name, score);  // 返回测试数据
                    })
                    .collect(Collectors.toList());  // 收集为列表
            }).get();  // 获取结果
        } catch (Exception e) {
            throw new RuntimeException(e);  // 抛异常
        } finally {
            pool.shutdown();  // 关闭线程池
        }
    }
}

// 测试数据类
class TestData {
    private final int id;  // ID
    private final String name;  // 名称
    private final double score;  // 分数
    
    public TestData(int id, String name, double score) {
        this.id = id;  // 初始化 ID
        this.name = name;  // 初始化名称
        this.score = score;  // 初始化分数
    }
    
    // getter 方法
    public int getId() { return id; }
    public String getName() { return name; }
    public double getScore() { return score; }
}

// 使用示例
ParallelDataGenerator generator = new ParallelDataGenerator();  // 创建生成器
List<TestData> data = generator.generateTestData(100_000, 4);  // 生成 10 万条数据,4 个线程
System.out.println("生成数据数量: " + data.size());  // 输出数量

并行数据生成用 SplittableRandom,每个线程用独立的生成器,性能好,数据质量也高。

ThreadLocalRandom:线程本地随机数

ThreadLocalRandom 是线程本地的随机数生成器,适合每个线程独立使用:

import java.util.concurrent.*;

// ThreadLocalRandom 使用
public class ThreadLocalRandomExample {
    public static void main(String[] args) {
        // 多个线程使用 ThreadLocalRandom
        for (int i = 0; i < 4; i++) {
            final int threadId = i;  // 线程 ID
            new Thread(() -> {
                // 每个线程获取自己的 ThreadLocalRandom
                ThreadLocalRandom random = ThreadLocalRandom.current();  // 获取线程本地随机数生成器
                
                for (int j = 0; j < 1000; j++) {
                    int value = random.nextInt(100);  // 生成随机数
                    System.out.println("线程 " + threadId + ": " + value);  // 输出随机数
                }
            }).start();
        }
    }
}

ThreadLocalRandom 是线程本地的,每个线程用独立的生成器,不需要同步,性能好。

性能对比

性能对比能看出不同方案的优势:

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;

// 性能对比
public class RandomPerformanceComparison {
    // 方案一:Random + 同步
    public static void testSynchronizedRandom(int iterations, int threads) {
        Random random = new Random();  // 共享的 Random
        Object lock = new Object();  // 锁对象
        CountDownLatch latch = new CountDownLatch(threads);  // 计数器
        
        long start = System.nanoTime();  // 开始时间
        
        for (int i = 0; i < threads; i++) {
            new Thread(() -> {
                for (int j = 0; j < iterations; j++) {
                    synchronized (lock) {
                        random.nextInt();  // 需要加锁
                    }
                }
                latch.countDown();  // 计数减一
            }).start();
        }
        
        try {
            latch.await();  // 等待所有线程完成
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();  // 恢复中断状态
        }
        
        long time = System.nanoTime() - start;  // 计算时间
        System.out.println("同步 Random: " + time / 1_000_000 + " ms");  // 输出时间
    }
    
    // 方案二:SplittableRandom
    public static void testSplittableRandom(int iterations, int threads) {
        SplittableRandom random = new SplittableRandom();  // 主生成器
        CountDownLatch latch = new CountDownLatch(threads);  // 计数器
        
        long start = System.nanoTime();  // 开始时间
        
        for (int i = 0; i < threads; i++) {
            SplittableRandom localRandom = random.split();  // 分割生成器
            new Thread(() -> {
                for (int j = 0; j < iterations; j++) {
                    localRandom.nextInt();  // 不需要同步
                }
                latch.countDown();  // 计数减一
            }).start();
        }
        
        try {
            latch.await();  // 等待所有线程完成
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();  // 恢复中断状态
        }
        
        long time = System.nanoTime() - start;  // 计算时间
        System.out.println("SplittableRandom: " + time / 1_000_000 + " ms");  // 输出时间
    }
    
    // 方案三:ThreadLocalRandom
    public static void testThreadLocalRandom(int iterations, int threads) {
        CountDownLatch latch = new CountDownLatch(threads);  // 计数器
        
        long start = System.nanoTime();  // 开始时间
        
        for (int i = 0; i < threads; i++) {
            new Thread(() -> {
                ThreadLocalRandom random = ThreadLocalRandom.current();  // 获取线程本地生成器
                for (int j = 0; j < iterations; j++) {
                    random.nextInt();  // 不需要同步
                }
                latch.countDown();  // 计数减一
            }).start();
        }
        
        try {
            latch.await();  // 等待所有线程完成
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();  // 恢复中断状态
        }
        
        long time = System.nanoTime() - start;  // 计算时间
        System.out.println("ThreadLocalRandom: " + time / 1_000_000 + " ms");  // 输出时间
    }
}

// 性能测试
int iterations = 1_000_000;  // 100 万次迭代
int threads = 4;  // 4 个线程

RandomPerformanceComparison.testSynchronizedRandom(iterations, threads);  // 测试同步 Random
RandomPerformanceComparison.testSplittableRandom(iterations, threads);  // 测试 SplittableRandom
RandomPerformanceComparison.testThreadLocalRandom(iterations, threads);  // 测试 ThreadLocalRandom

性能对比能看出 SplittableRandomThreadLocalRandom 的优势,不需要同步,性能好。

实际应用:并行游戏模拟

并行游戏模拟是 SplittableRandom 的典型应用:

import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;

// 并行游戏模拟
public class ParallelGameSimulation {
    private final SplittableRandom random;  // 随机数生成器
    
    public ParallelGameSimulation() {
        this.random = new SplittableRandom();  // 创建生成器
    }
    
    // 并行模拟游戏
    public List<GameResult> simulateGames(int gameCount, int threads) {
        ForkJoinPool pool = new ForkJoinPool(threads);  // 创建线程池
        
        try {
            return pool.submit(() -> {
                // 为每个线程创建独立的生成器
                return IntStream.range(0, gameCount)  // 创建范围流
                    .parallel()  // 并行处理
                    .mapToObj(i -> {
                        // 每个线程创建独立的生成器
                        SplittableRandom localRandom = random.split();  // 分割生成器
                        
                        // 模拟游戏
                        int player1Score = 0;  // 玩家 1 分数
                        int player2Score = 0;  // 玩家 2 分数
                        
                        // 模拟 10 轮
                        for (int round = 0; round < 10; round++) {
                            player1Score += localRandom.nextInt(1, 7);  // 玩家 1 掷骰子
                            player2Score += localRandom.nextInt(1, 7);  // 玩家 2 掷骰子
                        }
                        
                        // 判断胜负
                        String winner = player1Score > player2Score ? "玩家1" : 
                                       player1Score < player2Score ? "玩家2" : "平局";  // 判断胜负
                        
                        return new GameResult(i, player1Score, player2Score, winner);  // 返回游戏结果
                    })
                    .collect(Collectors.toList());  // 收集为列表
            }).get();  // 获取结果
        } catch (Exception e) {
            throw new RuntimeException(e);  // 抛异常
        } finally {
            pool.shutdown();  // 关闭线程池
        }
    }
}

// 游戏结果类
class GameResult {
    private final int gameId;  // 游戏 ID
    private final int player1Score;  // 玩家 1 分数
    private final int player2Score;  // 玩家 2 分数
    private final String winner;  // 获胜者
    
    public GameResult(int gameId, int player1Score, int player2Score, String winner) {
        this.gameId = gameId;  // 初始化游戏 ID
        this.player1Score = player1Score;  // 初始化玩家 1 分数
        this.player2Score = player2Score;  // 初始化玩家 2 分数
        this.winner = winner;  // 初始化获胜者
    }
    
    // getter 方法
    public int getGameId() { return gameId; }
    public int getPlayer1Score() { return player1Score; }
    public int getPlayer2Score() { return player2Score; }
    public String getWinner() { return winner; }
}

// 使用示例
ParallelGameSimulation simulation = new ParallelGameSimulation();  // 创建模拟器
List<GameResult> results = simulation.simulateGames(10_000, 4);  // 模拟 1 万局游戏,4 个线程

// 统计结果
long player1Wins = results.stream().filter(r -> r.getWinner().equals("玩家1")).count();  // 玩家 1 获胜次数
long player2Wins = results.stream().filter(r -> r.getWinner().equals("玩家2")).count();  // 玩家 2 获胜次数
long draws = results.stream().filter(r -> r.getWinner().equals("平局")).count();  // 平局次数

System.out.println("玩家1获胜: " + player1Wins);  // 输出玩家 1 获胜次数
System.out.println("玩家2获胜: " + player2Wins);  // 输出玩家 2 获胜次数
System.out.println("平局: " + draws);  // 输出平局次数

并行游戏模拟用 SplittableRandom,每个线程用独立的生成器,性能好,结果也正确。

注意事项和最佳实践

注意事项

  1. 不要共享生成器:多线程不要共享同一个生成器,每个线程用独立的生成器。

  2. 分割生成器:使用 split() 方法分割生成器,不要手动创建多个生成器。

  3. 线程安全SplittableRandomThreadLocalRandom 都是线程安全的,但不要跨线程共享。

  4. 性能考虑:并行计算时用 SplittableRandom,单线程用 ThreadLocalRandom

最佳实践

  1. 使用 SplittableRandom:并行计算场景用 SplittableRandom,性能好,功能强。

  2. 使用 ThreadLocalRandom:单线程场景用 ThreadLocalRandom,简单高效。

  3. 分割生成器:并行计算时用 split() 方法分割生成器,每个线程用独立的生成器。

  4. 避免同步:不要用同步的 Random,用 SplittableRandomThreadLocalRandom

  5. 文档说明:在代码注释中说明为什么选择某个方案,帮助其他开发者理解。

总结

多线程环境下生成随机数,增强的伪随机数生成器提供了 SplittableRandomThreadLocalRandom 两种方案,还有并行流支持,让你能根据需求选择合适的方案。这玩意儿对于并行计算、模拟、游戏这些需要大量随机数的场景特别有用。

建议在实际项目中试试,特别是需要高性能并行计算的场景。下一篇文章咱就聊聊恢复始终严格的浮点语义,看看怎么确保浮点运算的精确性。兄弟们有啥问题随时问,鹏磊会尽量解答。

本文章最后更新于 2025-11-28