Java中生成随机数常用的有下面这8种写法:简而言之,名称带安全的未必安全,名字简洁的未必简单。

  • Math.random()

  • Random

  • ThreadLocalRandom

  • SecureRandom

  • UUID.randomUUID()

  • RandomStringUtils

  • RandomUtils

  • RandomUtil

Math.random()

Talk is cheap, show me the code.  先上源码:

public static double random() {return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
private static final class RandomNumberGeneratorHolder {static final Random randomNumberGenerator = new Random();
}

从源码可以看出:Math.random本质上用的就是new Random。而且人家给了一个最佳实践:使用的时候不用每次就new一个对象,直接做成一个静态类就可以了。

Random

Random是伪随机数类,采用线性同余算法产生的。伪随机数是1946年冯诺依曼提出来的。他在参加一个氢弹的制造项目,需要大量的随机数模拟核聚变、核裂变。因为当时设备没办法去存储大量的随机数,所以就想到了这个办法。伪随机数(或称伪乱数)这个称呼是因为真随机数都不是计算出来的。这里是使用一个确定性的算法计算出来的似乎是随机的数序,因此伪随机数实际上并不随机。从更宏大的视角看来,是非常有规律的,就像我在《大话高可用》里提到的炊烟。

下面来说说这个线性同余算法,大家不用担心。我比大家还对算法一窍不通。但是我有人类的常识,这个常识是什么呢?既然是计算得到的,并且人类看起来没有规律,那一定有一个外部因子(种子),种子是一个变量。顺着这个思路看源码:

/*** Creates a new random number generator. This constructor sets* the seed of the random number generator to a value very likely* to be distinct from any other invocation of this constructor.*/
public Random() {this(seedUniquifier() ^ System.nanoTime());
}

没想到在构造函数里就给出了答案,这个因子就是系统时间,精确到纳秒。这里为了方便描述,我们举例想要一个int类型的随机数(实际它可以产生其他数值类型的随机数)。来看看源码:

public int nextInt() {return next(32);
}
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));
}

解释一下:调用nextInt默认会生成32个比特位的数字。调用next方法,会有oldseed和nextseed。计算出nextseed会使CAS,将nextseed替换现有oldseed。nextseed的计算方法是oldseed做一些运算,运算的其他数值都是常量。最终返回nextseed值。

因为使用了AtomicLong、自旋+CAS(可以参考《系统梳理一下锁》),所以Random生成随机数是线程安全的。new一个全局变量,下次不用新建,Math.random的调用方法是合理的。

通过这个源码大胆猜测一下:如果两个不同的进程或者线程,在纳秒级相同的时间同时调用new Random,这时候nextInt的返回值是相同的!下次调用nextInt的值也相同!想要验证的话,可以下载java源码,在new Random的地方稍作修改,

System.nanoTime()改成固定值,这里我就不验证了。

ThreadLocalRandom

上面提到Random使用了线程安全的算法:AtomicLong、自旋+CAS。这在保证线程安全的同时造成了很大的性能开销。ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争。

它不是直接用new实例化,而是第一次使用时初始化其静态方法current()。直接调用静态方法,可以每个线程实例化一个。有朋友测试过,100个线程的情况下,3分钟,Math.random可以跑几百次。而

ThreadLocalRandom.current().nextInt()可以跑十几万次!

来看一下它是怎么做到的:

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

}

static final void localInit() {int p = probeGenerator.addAndGet(PROBE_INCREMENT);int probe = (p == 0) ? 1 : p; // skip 0long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));Thread t = Thread.currentThread();UNSAFE.putLong(t, SEED, seed);UNSAFE.putInt(t, PROBE, probe);
}

简而言之,Random是通过系统时间这个外部因子,而ThreadLocalRandom都是通过UNSAFE调用本地方法拿到线程本身的一些变量作为外部因子。所有的参数绑定在线程本身,和其他线程没有竞争,所以可以不加锁就保证线程安全。

public int nextInt() {return mix32(nextSeed());

}

private static int mix32(long z) {z = (z ^ (z >>> 33)) * 0xff51afd7ed558ccdL;return (int)(((z ^ (z >>> 33)) * 0xc4ceb9fe1a85ec53L) >>> 32);
}
final long nextSeed() {Thread t; long r; // read and update per-thread seedUNSAFE.putLong(t = Thread.currentThread(), SEED,r = UNSAFE.getLong(t, SEED) + GAMMA);return r;
}

生成随机数计算的时候也都是调用线程内的变量,不加锁。大家可以举一反三一下,回忆一下ThreadLocal相关知识。

SecureRandom

在安全应用场景,随机数应该使用安全的随机数。密码学意义上的安全随机数,要求必须保证其不可预测性。

由于 Random 是采用时间作为随机数种子,如果黑客知道随机数产生的时间,那就能重现随机数。而 SecureRandom 属于强随机数,一般不单独采用时间作为随机数种子,除了系统时间,还会采用临时文件夹中大小、某个线程从休眠到被唤醒所耗的时间等等一系列无法重现的值作为随机数种子。

因为SecureRandom 采用了很多外部参数,会产生熵源不足时阻塞问题。在我做过的项目中,因为有的业务凌晨没有流量,这个问题实际发生过。建议有明显低谷的业务,低谷时低于1TPS时不要用。

像SecureRandom这种强随机在很多场景下效果不一定好。有一个小故事说itunes之前播歌的时候采用的是真随机。有些用户总是抱怨说你这是真随机吗?怎么来来回回给我播放那几首歌。苹果公司有苦说不出,于是把算法改成伪随机:洗牌算法,先把所有的歌打乱顺序,然后按顺序播放。改完之后用户纷纷点赞。

适合使用SecureRandom的场景,比如要给每个资源生成一个url,为了防止url规律被别人攻破,使用爬虫爬取到自己的资源可以用这个。

UUID.randomUUID()

看下面的代码就知道UUID.randomUUID()是基于SecureRandom

public static UUID randomUUID() {SecureRandom ng = Holder.numberGenerator;byte[] randomBytes = new byte[16];ng.nextBytes(randomBytes);randomBytes[6]  &= 0x0f;  /* clear version        */randomBytes[6]  |= 0x40;  /* set to version 4     */randomBytes[8]  &= 0x3f;  /* clear variant        */randomBytes[8]  |= 0x80;  /* set to IETF variant  */return new UUID(randomBytes);
}

有的朋友说不对呀,网上告诉我UUID是时间戳+时钟序列+MAC地址。注意上面代码注释有个set to version 4。

从UUID的类注释里就可以看到java 8提到4个版本:

The version field holds a value that describes the type of this {@code
* UUID}.  There are four different basic types of UUIDs: time-based, DCE
* security, name-based, and randomly generated UUIDs.  These types have a
* version value of 1, 2, 3 and 4, respectively.

翻译一下:

版本1 - 基于时间

版本2 - 基于DCE sercrity

版本3 - 基于名字(MD5)

版本4 - 基于随机数

记住啦:java默认UUID是基于随机数的!

证明一下:如果是基于时间戳的,生成的UUID前几位应该相同

@Test
public void test() {for (int i = 0; i < 3; i++) {System.out.println(UUID.randomUUID());}
}

结果:

a0858771-c903-4061-ba6a-53efc372d8a0

17ebdc58-db6c-452f-9b21-b4f54d3958a5

116d6a4f-e50a-4001-b94c-61596da3a75d

事实胜于记忆中的知识,它有可能是错的!

java UUID也支持版本3:

@Test
public void test() {for (int i = 0; i < 3; i++) {System.out.println(UUID.nameUUIDFromBytes("编程一生".getBytes()));}
}

运行结果:

743f24b2-0914-363b-ace1-f4da750dccad

743f24b2-0914-363b-ace1-f4da750dccad

743f24b2-0914-363b-ace1-f4da750dccad

这个在你的机器上执行也是这个结果,就是MD5了一下。

有人说版本1、版本2怎么用,好像还听说过版本5。可以用linux命令

uuid -n 3 -v1

运行结果:

5b01cea2-9561-11e9-965b-a3d050dd0f23

5b01cf60-9561-11e9-965c-1b66505f5845

5b01d118-9561-11e9-965d-97354eb9e914

linux命令版本可指定!

RandomStringUtils、RandomUtils和RandomUtil

这两个不是Java原生的方法,apache commons下有这两个工具类,本质上还是Random。跟进代码去看可以发现:RandomUtils用的是JVMRandom。这个只不过是封装了一个静态的Random,所有使用RandomUtils类的,全局只用一个,减少了新建对象成本。

如果你问我一般情况下建议用哪个,我选ThreadLocalRandom。那这么好的方法是不是应该也有对应的封装工具类呢?有的,hultool就搞了一个RandomUtil,

RandomUtil.getRandom()返回的就是ThreadLocalRandom。

RandomUtil.getSecureRandom()返回的是SecureRandom,提供多种选择。

总结

一张图表示他们之间的关系:

在《CURD系统怎么做出技术含量--怎样引导面试》里我提到可以利用工作中总结的技巧惊艳面试官,如果能把一个问题系统性讲明白,甚至让面试官意识到之前的知识竟然是错的,也一定能让面试官眼前一亮。

总结了很多有关于java面试的资料,希望能够帮助正在学习java的小伙伴。由于资料过多不便发表文章,创作不易,望小伙伴们能够给我一些动力继续创建更好的java类学习资料文章,
请多多支持和关注小作,别忘了点赞+评论+转发。右上角私信我回复【03】即可领取免费学习资料谢谢啦!

惊艳面试官-Java中关于随机数生成8种方式的思考相关推荐

  1. 【面试】JAVA中交换值的三种方式

    方式一:异或法 int i = 5, j = 9; i = i ^ j; j = i ^ j; -> j = i ^ j ^ j; -> j = i; i = i ^ j; -> i ...

  2. 【客服狂收offer秘籍】惊艳面试官的自我介绍

    作为一名被面试过也面试过别人N次的客服老司机,我算是对面试时的自我介绍有一些独特的见解.今天特意写一篇[自我介绍]的超全攻略,教大家怎么准备一份满分的自我介绍,惊艳面试官! 一.自我介绍原则 1.自我 ...

  3. 12月18日云栖精选夜读 | Java 中创建对象的 5 种方式!...

    作为Java开发者,我们每天创建很多对象,但我们通常使用依赖管理系统,比如Spring去创建对象.然而这里有很多创建对象的方法,我们会在这篇文章中学到. Java中有5种创建对象的方式,下面给出它们的 ...

  4. Java中创建对象的几种方式

    Java中创建对象的几种方式 1.使用new创建对象,在堆上创建. 2.克隆 3.反序列化 4.反射创建对象 5.NIO中可以使用本地方法直接分配堆外内存. 转载于:https://www.cnblo ...

  5. Java中创建对象的四种方式

    为什么80%的码农都做不了架构师?>>>    Java中创建对象的四种方式 (1) 用new语句创建对象,这是最常见的创建对象的方法.    (2) 运用反射手段,调用java.l ...

  6. java声明异常必须抛出吗_简述java中抛出异常的两种方式

    java编程中经常遇到异常,这时就需要利用java中的异常抛出机制,在java中提供了两种抛出异常的方法:try{}  catch() {}和throw. 一.抛出异常的两种方式 (1) 首先我们来看 ...

  7. CURD系统怎么做出技术含量惊艳面试官完善版

    引子 十几年前的一天,有段时间在读张爱玲的小说,同事看见了就说原来你爱看这种东西.我到现在都不知道他说的是哪种东西.那时候我只是单纯的觉得张爱玲写的东西有种特质,这种特质掌握了做什么事情都不会太差.后 ...

  8. 想要惊艳面试官?你一张嘴就输了

    网申,校招,社招,内推-- 找工作的途径不止一个 但无论如何选择都逃不掉面试关 那么如何能在面试中表现 才能惊天地,泣鬼神--惊艳到面试官 让他愿意轻易给你提供offer? (一副认真脸) 想要在面试 ...

  9. 面试官 | Java中的注解是如何工作的?

    自Java5.0版本引入注解之后,它就成为了Java平台中非常重要的一部分.开发过程中,我们也时常在应用代码中会看到诸如@Override,@Deprecated这样的注解.这篇文章中,我将向大家讲述 ...

最新文章

  1. 什么是区块链智能合约?
  2. Swift3.0语言教程使用字符串创建和初始化字符串
  3. Openshift API部分源码学习笔记(一)
  4. js 操作java对象_JavaScript 对象基础
  5. php的qq接口文档,分账接口
  6. vs2017创建外部css,webpack - 从生成的Microsoft SPA模板VS2017 webpack中删除特定供应商css - 堆栈内存溢出...
  7. 每日算法系列【LeetCode 658】找到 K 个最接近的元素
  8. Linux下PHP5.5编译参数详解
  9. 在线扒站复活版可预览网站html源码
  10. Email,电子邮箱免费注册流程
  11. win10中文用户名怎么改成英文文件夹路径
  12. vant 验证手机号_Vue 正则表达式验证邮箱和手机号码
  13. 免杀实战之面向PHP的WebShell免杀
  14. 尚硅谷周阳老师2020最新Springcloud完整版学习
  15. 3.MATLAB实现有限脉冲响应数字滤波器(FIR)
  16. 要玩就玩最好的棋牌游戏
  17. 阿里云服务器安装tomcat
  18. 万物皆可集成系列:低代码释放用友深度价值(1)—系统对接集成
  19. 数据手册中的英语小屁孩
  20. 用c语言试完成九九乘法口诀表中倒直三角形,

热门文章

  1. 皇室战争挑战模式锦标赛概率分析
  2. 基于JAVA工资管理系统计算机毕业设计源码+数据库+lw文档+系统+部署
  3. 2010南非世界杯电视直播表
  4. js实现数字以5为单位向上取整/js时间以10分钟向上取整
  5. Python-自动化测试-多表单
  6. mysql 中的几种联接方式
  7. linux7如何关闭防火墙,Linux下Centos 7如何关闭防火墙?
  8. 【C语言博客作业06】
  9. 攀升 A3 刀锋一体机评测
  10. 使用Faster R-CNN(linux-pytorch)训练自己的数据集过程记录