【小家java】Java中Random ThreadLocalRandom 设置随机种子获取随机数精讲

我们都知道,随机数在太多的地方使用了,比如加密、混淆数据等,我们使用随机数是期望获得一个唯一的、不可仿造的数字,以避免产生相同的业务数据造成混乱。
在Java项目中通常是通过Math.random方法和Random类来获得随机数的。那么本文针对于这两种产生随机数的方法进行源码级别的精度,让你以后不再犯错。

先说平时使用

绝大多数情况,我们其实是想通过此类来生成一个随机整数。此处不鳌诉推倒过程,直接公布一个公式:

Random rand = new Random();
int num = rand.nextInt(MAX - MIN + 1) + MIN;

因此,我们需要[min,max]之间的随机数,直接这么来使用就ok了 闭区间哦

java产生随机数的几种方式

1.使用Math.random()方法来产生一个随机数,这个产生的随机数是0-1之间的一个double,我们可以把他乘以一定的数,比如说乘以100,他就是个100以内的随机
2.使用java.util这个包里面提供了一个Random的类(最常用)
3.使用currentTimeMillis的取模算法(使用较少)

java中通过这几种方法产生的随机数叫伪随机数,并不是真正的随机数。这里简单科普一下伪随机和真随机的区别:

伪随机(preundorandom):通过算法产生的随机数都是伪随机!!
真随机:比如,通过机器的硬件噪声产生随机数、通过大气噪声产生随机数。(只有通过真实的随机事件产生的随机数才是真随机)

一、java.lang.Math.Random;

调用这个Math.Random()函数能够返回带正号的double值,该值大于等于0.0且小于1.0,即取值范围是[0.0,1.0)的左闭右开区间,返回值是一个伪随机选择的数,在该范围内(近似)均匀分布

 public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            int num = (int) (Math.random() * 10);
            System.out.println("num is:" + num);
        }
    }
输出:
num is:1
num is:4
num is:3
第二次输出:
num is:0
num is:6
num is:5

我们会发现,每次输出的值都是不一样的,但是都保持在[0,10)的区间里面

在使用Math.Random()的时候需要注意的地方时该函数是返回double类型的值,所以在要赋值给其他类型的变量的时候注意需要进行类型转换

二、java.util.Random;

1、java.util.Random类中实现的随机算法是伪随机,也就是有规则的随机,所谓有规则的就是在给定种(seed)的区间内随机生成数字(后面会有例子验证这一点);
2、相同种子的Random对象,相同次数生成的随机数字是完全相同的(所以才叫伪随机嘛);
3、Random类中各方法生成的随机数字都是均匀分布的,也就是说区间内部的数字生成的几率均等;

它提供了两种构造函数:

  • 一、Random( ):无参构造方法(不设置种子)
    • 虽然表面上看我们未设置种子,但Random构造方法里有一套自己的种子生成机制。参照内部源码如下
public Random() {
        this(seedUniquifier() ^ System.nanoTime());
    }

    private static long seedUniquifier() {
        // L'Ecuyer, "Tables of Linear Congruential Generators of
        // Different Sizes and Good Lattice Structure", 1999
        for (;;) {
            long current = seedUniquifier.get();
            long next = current * 181783497276652981L;
            if (seedUniquifier.compareAndSet(current, next))
                return next;
        }
    }

    private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);

这个其实就是取当前毫秒值,然后通过“线性同余算法”来生成的一个数字。下面摘抄了生成种子的过程,仅供参考:

1、获得一个长整形数作为“初始种子”(系统默认的是8682522807148012L)
2、不断与一个变态的数——181783497276652981L相乘(天知道这些数是不是工程师随便滚键盘滚出来的-.-)得到一个不能预测的值,直到 能把这个不能事先预期的值 赋给Random对象的静态常量seedUniquifier 。因为多线程环境下赋值操作可能失败,就for(;;)来保证一定要赋值成功
3、与系统随机出来的nanotime值作异或运算,得到最终的种子

因为取了机器当前的纳秒值nanoTime,所以不管运行多少次,出现的值都是不一样的。但是由于这随机数是通过算法计算出来的,所以其实是有规律性的,如果精通这个算法,是可以推测出下一个值是什么,所以我们才叫这种叫伪随机数。

  • 二、Random(long seed) 有参构造方法(设置种子)
public Random(long seed) {
        if (getClass() == Random.class)
            this.seed = new AtomicLong(initialScramble(seed));
        else {
            // subclass might have overriden setSeed
            this.seed = new AtomicLong();
            setSeed(seed);
        }
    }

    private static long initialScramble(long seed) {
        return (seed ^ multiplier) & mask;
    }

从源码可以看出,我们可以手动的给传递一个种子进去,然后通过线形算法计算我们的种子,构造一个long值即可。

  public static void main(String[] args) {
        Random r = new Random(1000);
        for (int i = 1; i < 4; i++) {
            System.out.println("第" + i + "次:" + r.nextInt());
        }
    }
输出:
第1次:-12447463212次:10604938713次:-1826063944
第二次输出:
第1次:-12447463212次:10604938713次:-1826063944

我们发现,不管运行多少次,规律都是一模一样的,再看这个例子

public static void main(String[] args) {
        Random r = new Random(1000);
        for (int i = 1; i < 4; i++) {
            System.out.println("第" + i + "次:" + r.nextInt());
        }

        Random r2 = new Random(1000);
        for (int i = 1; i < 4; i++) {
            System.out.println("第" + i + "次:" + r2.nextInt());
        }
    }
输出:
第1次:-12447463212次:10604938713次:-18260639441次:-12447463212次:10604938713次:-1826063944

我们发现,哪怕是两个random对象,只要种子一样,输出的随机数都是一样的,所以一定要慎用种子啊。

这里同样的代码,只要你不换机器,运行多少次都是相同的。但是如果换一台机硬件机器,就不同了哟。需要了解这里面的原理。种子不同,产生不同的随机数。种子相同,即使实例不同也产生相同的随机数。

new Random(1000)显式地设置了随机种子为1000,运行多次,虽然实例不同,但都会获得相同的三个随机数。所以,除非必要,否则不要设置随机种子。


虽然二者都是伪随机,但是,无参数构造方法(不设置种子)具有更强的随机性,能够满足一般统计上的随机数要求。使用有参的构造方法(设置种子)无论你生成多少次,每次生成的随机序列都相同,名副其实的伪随机!!

最后再来简单对比一下这两个随机函数到底的特点:

  • 1.java.Math.Random()实际是在内部调用java.util.Random()的,它有一个致命的弱点,它和系统时间有关,也就是说相隔时间很短(短到种子相同)的两个random比如: double a = Math.random();double b = Math.random(); 即有可能会得到两个一模一样的double。
  • 2.java.util.Random()在调用的时候可以实现和java.Math.Random()一样的功能,而且他具有很多的调用方法,相对来说比较灵活。所以从总体来看,使用java.util.Random()会相对来说比较灵活一些。

写到最后:Random和ThreadLocalRandom的用法和区别

  • Random:生产一个伪随机数(通过相同的种子,产生的随机数是相同的)。
  • ThreadLocalRandom:是java7新增类,是Random的子类,在多线程并发情况下,ThreadLocalRandom相对于Random可以减少多线程资源竞争,保证了线程的安全性。public class ThreadLocalRandom extends Random因为构造器是默认访问权限,只能在java.util包中创建对象,故提供了一个方ThreadLocalRandom.current()用于返回当前类的对象.
package java.util.concurrent;
public class ThreadLocalRandom extends Random {}

我们发现他在concurrent包下,所以他肯定就是为并发而生的。下面卡个使用案例:

  public static void main(String[] args) {
        ThreadLocalRandom threadRandom = ThreadLocalRandom.current();
        System.out.println(threadRandom.nextInt(10));
        System.out.println("-----------产生两个数之间的随机数----------------");
        System.out.println(threadRandom.nextInt(10, 100));
        System.out.println("---------------------------");
        //随机生成UUID
        String uuid = UUID.randomUUID().toString();
        System.out.println(uuid);
    }
输出:
3
-----------产生两个数之间的随机数----------------
62
---------------------------
cb688869-24a3-457f-abc9-a2e0687c5e66
下面从性能方面,来看看ThreadLocalRandom 这个哥们的优势

ThreadLocalRandom类是JDK7在JUC包下新增的随机数生成器,它解决了Random类在多线程下多个线程竞争内部唯一的原子性种子变量而导致大量线程自旋重试的不足。

先给出个结论:ThreadLocalRandom使用ThreadLocal的原理,让每个线程内持有一个本地的种子变量,该种子变量只有在使用随机数时候才会被初始化,多线程下计算新种子时候是根据自己线程内维护的种子变量进行更新,从而避免了竞争。


因此,互联网分布式环境下,建议使用此类来代替Random类来提高效率
至于具体原因,从源码级别分析的内容,这里推荐一篇文章参考:ThreadLocalRandom类原理剖析

自1.0就已经存在,是一个线程安全类,理论上可以通过它同时在多个线程中获得互不相同的随机数,这样的线程安全是通过AtomicLong实现的。
Random使用AtomicLong CAS(compare and set)操作来更新它的seed,尽管在很多非阻塞式算法中使用了非阻塞式原语,CAS在资源高度竞争时的表现依然糟糕,后面的测试结果中可以看到它的糟糕表现。


使用一个普通的long而不是使用Random中的AtomicLong作为seed
不能自己创建ThreadLocalRandom实例,因为它的构造函数是私有的,可以使用它的静态工厂ThreadLocalRandom.current()
它是CPU缓存感知式的,使用8个long虚拟域来填充64位L1高速缓存行

猜你喜欢

转载自blog.csdn.net/f641385712/article/details/81115164