多线程环境下生成随机数,鹏磊最烦的就是线程安全问题。用 Random 得加锁,性能差;用 ThreadLocalRandom 功能有限;用 SplittableRandom 又不知道怎么用。JDK 17 的增强伪随机数生成器终于解决了这些问题,提供了多种多线程安全的方案,性能也好。
多线程环境下生成随机数是个常见需求,但传统方式要么性能差,要么功能有限。增强的伪随机数生成器提供了 SplittableRandom 和 ThreadLocalRandom 两种方案,还有并行流支持,让你能根据需求选择合适的方案。这玩意儿对于并行计算、模拟、游戏这些需要大量随机数的场景特别有用。
多线程随机数生成的问题
多线程环境下生成随机数,有几个常见问题:
问题一:线程安全
传统 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
性能对比能看出 SplittableRandom 和 ThreadLocalRandom 的优势,不需要同步,性能好。
实际应用:并行游戏模拟
并行游戏模拟是 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,每个线程用独立的生成器,性能好,结果也正确。
注意事项和最佳实践
注意事项
-
不要共享生成器:多线程不要共享同一个生成器,每个线程用独立的生成器。
-
分割生成器:使用
split()方法分割生成器,不要手动创建多个生成器。 -
线程安全:
SplittableRandom和ThreadLocalRandom都是线程安全的,但不要跨线程共享。 -
性能考虑:并行计算时用
SplittableRandom,单线程用ThreadLocalRandom。
最佳实践
-
使用 SplittableRandom:并行计算场景用
SplittableRandom,性能好,功能强。 -
使用 ThreadLocalRandom:单线程场景用
ThreadLocalRandom,简单高效。 -
分割生成器:并行计算时用
split()方法分割生成器,每个线程用独立的生成器。 -
避免同步:不要用同步的
Random,用SplittableRandom或ThreadLocalRandom。 -
文档说明:在代码注释中说明为什么选择某个方案,帮助其他开发者理解。
总结
多线程环境下生成随机数,增强的伪随机数生成器提供了 SplittableRandom 和 ThreadLocalRandom 两种方案,还有并行流支持,让你能根据需求选择合适的方案。这玩意儿对于并行计算、模拟、游戏这些需要大量随机数的场景特别有用。
建议在实际项目中试试,特别是需要高性能并行计算的场景。下一篇文章咱就聊聊恢复始终严格的浮点语义,看看怎么确保浮点运算的精确性。兄弟们有啥问题随时问,鹏磊会尽量解答。