Random和ThreadLocalRandom原理分析

Random与ThreadLocalRandom深入解析
本文详细对比了Random和ThreadLocalRandom两种随机数生成器的实现原理。Random使用CAS保证多线程环境下随机数的独立性,但可能引发线程自旋降低效率。ThreadLocalRandom为每个线程分配独立的seed,避免了线程间竞争,提高了并发性能。

Random和ThreadLocalRandom都是用于生成随机数的。用法比较简单,这里直接看代码分析原理。

Random

Random随机数的实现几乎都是依赖于内部的next方法,都是在next的基础上进行额外的计算,这里直接看next实现:

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

Random获取随机数分两步:

  1. 获得老的种子数据
  2. 通过老的种子计算新的种子

最后,用老的种子替换新的种子,这个我们都能理解,但是这里使用的是CAS进行替换,这个是为什么呢?

如果不用CAS,再看上面代码,如果是多个线程同时并行访问,会出现:多个线程同时获得了同一个老的种子,然后根据老的种子计算出同样的新的种子,这就导致了多个线程获得的随机数相同。

所以,介于这种情况,使用CAS来进行控制,使得在多线程的情况下,保证每个线程获取的随机数是不同的。如果CAS失败,说明老的种子已经被使用,所以会继续循环重新拿到最新的老的种子再去计算新的种子,直到成功并返回。

Random确实很好的解决了多线程下的问题,但是这又带来了新的问题:在线程竞争激烈的情况下,多个线程通过CAS竞争一个seed,导致大多数线程都在自旋,严重降低了多线程下获取随机数的效率,所以ThreadLocalRandom就是为了解决这个问题。

ThreadLocalRandom

初始化

ThreadLocalRandom的原理其实也很简单。既然Random多线程共享seed,竞争激烈导致效率低,那为什么不每个线程都拥有自己的seed呢,每个线程每次通过自己的当前seed获取新的seed呢?是的,ThreadLocalRandom就是这样做的。

那每个线程如果都拥有自己的seed,首先每个线程的seed保存在哪儿呢?不用多想,当前是当前线程对象了(ThreadLocal在线程本地也有自己的ThreadLocalMap,原理是类似的)。
为了实现ThreadLocalRandom,在Thread中有如下字段:

 /** The current seed for a ThreadLocalRandom */
 //用于产生随机数的种子
 @sun.misc.Contended("tlr")
 long threadLocalRandomSeed;
 /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
 //如果threadLocalRandomSeed被初始化则threadLocalRandomProbe不为0
 @sun.misc.Contended("tlr")
 int threadLocalRandomProbe;
 /** Secondary seed isolated from public ThreadLocalRandom sequence */
 @sun.misc.Contended("tlr")
 //用来生成threadLocalRandomSeed的辅助种子
 int threadLocalRandomSecondarySeed;

从Thread中字段的定义来看,threadLocalRandomSeed是没有初始值得。可想而知:只有用到ThreadLocalRandom的时候,才需要threadLocalRandomSeed,需要的时候再进行初始化,避免不必要的内存消耗。

ThreadLocalRandom是在第一次调用current的时候,对当前threadLocalRandomSeed进行的初始化的,并且ThreadLocalRandom是单例模式:

public static ThreadLocalRandom current() {
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
    return instance;
}

对上面代码中的UNSAFE.getInt(Thread.currentThread(), PROBE)不理解的,这里简单讲一下unsafe相关的知识。先看这句代码,找到UNSAFE和PROBE到底是什么,在ThreadLocalRandom中有如下定义:

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();
        Class<?> tk = Thread.class;
        SEED = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSeed"));
        PROBE = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomProbe"));
        SECONDARY = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
    } catch (Exception e) {
        throw new Error(e);
    }
}

可以看到,UNSAFE的类型是sun.misc.Unsafe,而PROBE,通过UNSAFE.objectFieldOffset获取的。其实,在unsafe中,定义了很多的直接内存访问操作,操作更高效,但是也存在一定的风险。unsafe可以通过getDeclaredField获得某个对象中某个字段的偏移量,然后可以直接通过字段偏移量获取某个对象的字段值,也可以通过字段偏移量给对象的字段赋值。当然,unsafe的功能不止于此,对于unsafe更详细的介绍可以直接看 Unsafe应用解析
这里就是,先通过static初始化拿到线程中threadLocalRandomSeed等变量的偏移量,然后在用到这些变量的时候,通过初始化得到的偏移量,使用get和put取值和赋值操作。

好,现在继续回过头看ThreadLocalRandom的创建:

public static ThreadLocalRandom current() {
	//拿到当前线程对象中threadLocalRandomProbe的值,如果为0(默认值就是0),说明初始种子没有初始化,所以需要先进行初始化。
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
   	//instance是static,ThreadLocalRandom 是单例模式,因为seed保存在当前的线程中,所以没必要每次都创建一个ThreadLocalRandom 
    return instance;
}
static final void localInit() {
	//重新计算threadLocalRandomProbe,因为已经初始化了,所以threadLocalRandomProbe一定不能为0了。
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    int probe = (p == 0) ? 1 : p; // skip 0
    //计算初始种子,这里关键是seeder。
    //seeder是静态变量,在类加载的时候会初始化一个最初始的值。
    //这里在计算seed的值得同时,每来一个线程进行初始化,这里都会为seeder的值加SEEDER_INCREMENT,保证下一个线程的seeder和前一个的不一样。
    //所以当所有线程第一次同时来竞争初始化的时候,会存在竞争,初始化之后,就不会再竞争了。
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    Thread t = Thread.currentThread();
    //将计算后的初始化值通过unsafe赋值给当前线程的变量
    UNSAFE.putLong(t, SEED, seed);
    UNSAFE.putInt(t, PROBE, probe);
}

初始化种子,关键是seeder,seeder定义与初始化如下:

private static final AtomicLong seeder = new AtomicLong(initialSeed());

private static long initialSeed() {
	//这儿可以配置启动参数来选择通过哪种方式进行初始化seeder。
    String sec = VM.getSavedProperty("java.util.secureRandomSeed");
    if (Boolean.parseBoolean(sec)) {
        byte[] seedBytes = java.security.SecureRandom.getSeed(8);
        long s = (long)(seedBytes[0]) & 0xffL;
        for (int i = 1; i < 8; ++i)
            s = (s << 8) | ((long)(seedBytes[i]) & 0xffL);
        return s;
    }
    return (mix64(System.currentTimeMillis()) ^
            mix64(System.nanoTime()));
}

可以看到,这里seeder是静态变量,所以在类加载的时候,就初始化好了。因为每个线程调用localInit都会改变seeder的值,也就保证了每个线程的seeder是不一样的。

nextSeed

ThreadLocalRandom获取随机数,基本都是通过nextSeed实现的,nextSeed代码如下,和Random原理相同,分为三步:

final long nextSeed() {
    Thread t; long r; // read and update per-thread seed
    //1、获取老的种子
    //2、计算新的种子
    //3、将新的种子覆盖老的种子
    UNSAFE.putLong(t = Thread.currentThread(), SEED,
                   r = UNSAFE.getLong(t, SEED) + GAMMA);
    return r;
}
  1. 获取老的种子 : UNSAFE.getLong(t, SEED)
  2. 计算新的种子:r = UNSAFE.getLong(t, SEED) + GAMMA
  3. 将新的种子覆盖老的种子 UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA);

和Random的区别就是:种子保存在当前线程对象中。

<think>首先,用户的问题是关于JavaRandom类的性能安全性问题。我需要根据系统级指令来构建我的回答。 系统级指令: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 - 回答结构清晰,帮助用户逐步解决问题。 - 参考提供的引用内容。 用户的问题:"我想了解JavaRandom类性能安全性分析" 参考引用: - [^1]: 讨论Random类的算法(线性同余法),可预测性,安全性问题,建议在安全应用中使用SecureRandom。 - [^2]: 描述Random类的实现细节,包括seed的更新函数,以及可能导致性能问题(如抢红包接口问题)。 - [^3]: 比较RandomThreadLocalRandom的性能,ThreadLocalRandom更快。 我的回答结构: 1. **介绍Random类**:简要解释JavaRandom类是什么,以及它的基本用途。 2. **性能分析**:讨论Random类的性能问题,包括引用中的内容。 3. **安全性分析**:讨论Random类的安全性问题,强调其可预测性。 4. **优化建议**:提供改进性能安全性的建议,比如使用ThreadLocalRandom或SecureRandom。 5. **生成相关问题**:在最后添加相关问题部分。 使用中文回答,确保语言流畅。 确保行内数学表达式正确:例如,如果提到算法公式,用$...$。例如,线性同余法的公式:$nextseed = (oldseed \times multiplier + addend) \& mask$。 从引用中提取关键点: - 性能:Random类在多线程环境可能有性能问题,因为seed是共享的AtomicLong,导致CAS操作竞争(引用[^2])。ThreadLocalRandom性能更好(引用[^3])。 - 安全性:Random使用线性同余法(LCG),是可预测的,不适合安全敏感应用(引用[^1])。建议使用SecureRandom。 回答结构: - 逐步解释: - 第一步:Random类的基本原理。 - 第二步:性能问题分析。 - 第三步:安全性问题分析。 - 第四步:解决方案最佳实践。 在引用末尾添加标识:例如,在讨论性能问题时,提到引用[^2][^3];在安全性时,提到引用[^1]。 最后,生成相关问题: - 基于主题,如如何优化Random的使用、SecureRandom的细节等。 完整回答框架: 1. **引言**:解释Java Random类的作用。 2. **性能分析**: - 描述算法实现。 - 讨论多线程性能问题(引用CAS操作)。 - 比较ThreadLocalRandom(引用性能数据)。 3. **安全性分析**: - 解释线性同余法的可预测性。 - 强调不适合安全应用。 - 建议SecureRandom。 4. **总结建议**:推荐在不同场景下的使用。 5. **相关问题**:列出3-5个相关问题。 确保数学表达式: - 例如,线性同余公式:$s_{n+1} = (a \times s_n + c) \mod m$,其中a、c、m是常量。 - 在文本中,用$...$包裹。 最终输出: - 用户可见层:回答内容。 - 结束时添加相关问题。</think>### Java Random类性能安全性分析 Java中的`Random`类(位于`java.util.Random`)是一个常用的伪随机数生成器(PRNG),用于生成随机序列。它在许多应用中被广泛使用,但存在性能安全性问题。我将逐步分析这些问题,并提供优化建议。分析基于`Random`类的实现机制实际应用场景。 #### 1. **Random类的基本原理** `Random`类使用线性同余生成器(LGC)算法生成随机数。其核心公式为: $$s_{n+1} = (a \times s_n + c) \mod m$$ 其中,$s_n$是当前种子(seed),$a$(multiplier)、$c$(addend)$m$(mask)是固定常量。默认情况下,种子基于系统时间初始化(如`System.nanoTime()`)。每次生成随机数时,种子通过原子操作更新[^1][^2]。 示例代码: ```java Random rand = new Random(); int randomNum = rand.nextInt(100); // 生成0-99的随机整数 ``` 这种算法简单高效,但存在固有缺陷,下面详细分析。 #### 2. **性能分析** `Random`类在多线程环境下性能较差,主要原因在于其内部状态(种子)的共享同步机制: - **种子更新竞争**:`Random`使用`AtomicLong`存储种子,每次调用`next()`方法时,通过CAS(Compare-and-Swap)操作更新种子: ```java nextseed = (oldseed * multiplier + addend) & mask; ``` 在多线程高并发场景(如抢红包接口),CAS操作可能导致大量线程竞争重试,增加CPU开销延迟。引用[^2]中提到,这会造成严重的性能瓶颈,例如在生成1000个随机数时,平均耗时约772微秒(基于JMH测试)。 - **ThreadLocalRandom对比**:Java 7引入了`ThreadLocalRandom`类(`java.util.concurrent.ThreadLocalRandom`),它为每个线程维护独立的种子,避免了共享状态竞争。测试显示,生成相同数量的随机数时,`ThreadLocalRandom`仅需约625微秒,性能提升约20%[^3]。 **建议**:在并发应用中优先使用`ThreadLocalRandom`,例如: ```java int randomNum = ThreadLocalRandom.current().nextInt(100); ``` 性能问题总结: - **优点**:单线程下性能良好,适合低并发场景。 - **缺点**:多线程时CAS竞争导致吞吐量下降,不适合高并发系统[^2][^3]。 #### 3. **安全性分析** `Random`类的安全性主要涉及其生成序列的可预测性: - **可预测性风险**:LGC算法是确定性的,即给定相同种子,输出序列完全可预测。攻击者如果获取初始种子(如通过时间戳推测),就能重现所有随机数。引用[^1]强调,在安全敏感应用(如加密密钥生成、支付系统)中,这会导致严重漏洞。 - **SecureRandom对比**:Java提供了`SecureRandom`类(`java.security.SecureRandom`),它基于密码学安全算法(如SHA1PRNG或NativePRNG),使用不可预测的熵源(如硬件噪声)。`SecureRandom`通过了NIST安全标准,但性能较低(生成速度比`Random`慢10倍以上)。 **建议**:在需要安全性的场景(如会话令牌或加密操作),必须使用`SecureRandom`: ```java SecureRandom secureRand = new SecureRandom(); byte[] key = new byte[16]; secureRand.nextBytes(key); // 生成安全随机字节 ``` 安全性问题总结: - **优点**:简单易用,适合非安全场景(如游戏随机事件)。 - **缺点**:算法可预测,不适合任何涉及隐私或安全的用途[^1]。 #### 4. **总结最佳实践** - **性能优化**:在并发环境使用`ThreadLocalRandom`;单线程应用可继续用`Random`。 - **安全强化**:始终用`SecureRandom`替代`Random`处理安全相关任务。 - **综合建议**: - 测试场景:用JMH工具(Java Microbenchmark Harness)评估随机数生成性能。 - 避免陷阱:不要依赖`Random`的默认种子(如`new Random()`),在需要可重现序列时(如测试),显式指定种子(`new Random(seed)`)。 通过以上分析,`Random`类在性能安全性上存在局限,但通过合理选择替代类,可以平衡效率风险[^1][^2][^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值