惊艳面试官-Java中关于随机数生成8种方式的思考
lipiwang 2024-11-12 13:16 10 浏览 0 评论
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 0
long 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 seed
UNSAFE.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系统怎么做出技术含量--怎样引导面试》里我提到可以利用工作中总结的技巧惊艳面试官,如果能把一个问题系统性讲明白,甚至让面试官意识到之前的知识竟然是错的,也一定能让面试官眼前一亮。
原文链接:https://mp.weixin.qq.com/s/9ReYiLCyvJKPXK07fXmrvg
相关推荐
- 前端入门——css 网格轨道详细介绍
-
上篇前端入门——cssGrid网格基础知识整体大概介绍了cssgrid的基本概念及使用方法,本文将介绍创建网格容器时会发生什么?以及在网格容器上使用行、列属性如何定位元素。在本文中,将介绍:...
- Islands Architecture(孤岛架构)在携程新版首页的实践
-
一、项目背景2022,携程PC版首页终于迎来了首次改版,完成了用户体验与技术栈的全面升级。作为与用户连接的重要入口,旧版PC首页已经陪伴携程走过了22年,承担着重要使命的同时,也遇到了很多问题:维护/...
- HTML中script标签中的那些属性
-
HTML中的<script>标签详解在HTML中,<script>标签用于包含或引用JavaScript代码,是前端开发中不可或缺的一部分。通过合理使用<scrip...
- CSS 中各种居中你真的玩明白了么
-
页面布局中最常见的需求就是元素或者文字居中了,但是根据场景的不同,居中也有简单到复杂各种不同的实现方式,本篇就带大家一起了解下,各种场景下,该如何使用CSS实现居中前言页面布局中最常见的需求就是元...
- CSS样式更改——列表、表格和轮廓
-
上篇文章主要介绍了CSS样式更改篇中的字体设置Font&边框Border设置,这篇文章分享列表、表格和轮廓,一起来看看吧。1.列表List1).列表的类型<ulstyle='list-...
- 一文吃透 CSS Flex 布局
-
原文链接:一文吃透CSSFlex布局教学游戏这里有两个小游戏,可用来练习flex布局。塔防游戏送小青蛙回家Flexbox概述Flexbox布局也叫Flex布局,弹性盒子布局。它决定了...
- css实现多行文本的展开收起
-
背景在我们写需求时可能会遇到类似于这样的多行文本展开与收起的场景:那么,如何通过纯css实现这样的效果呢?实现的难点(1)位于多行文本右下角的展开收起按钮。(2)展开和收起两种状态的切换。(3)文本...
- css 垂直居中的几种实现方式
-
前言设计是带有主观色彩的,同样网页设计中的css一样让人摸不头脑。网上列举的实现方式一大把,或许在这里你都看到过,但既然来到这里我希望这篇能让你看有所收获,毕竟这也是前端面试的基础。实现方式备注:...
- WordPress固定链接设置
-
WordPress设置里的最后一项就是固定链接设置,固定链接设置是决定WordPress文章及静态页面URL的重要步骤,从站点的SEO角度来讲也是。固定链接设置决定网站URL,当页面数少的时候,可以一...
- 面试发愁!吃透 20 道 CSS 核心题,大厂 Offer 轻松拿
-
前端小伙伴们,是不是一想到面试里的CSS布局题就发愁?写代码时布局总是对不齐,面试官追问兼容性就卡壳,想跳槽却总被“多列等高”“响应式布局”这些问题难住——别担心!从今天起,咱们每天拆解一...
- 3种CSS清除浮动的方法
-
今天这篇文章给大家介绍3种CSS清除浮动的方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。首先,这里就不讲为什么我们要清楚浮动,反正不清除浮动事多多。下面我就讲3种常用清除浮动的...
- 2025 年 CSS 终于要支持强大的自定义函数了?
-
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!1.什么是CSS自定义属性CSS自...
- css3属性(transform)的一个css3动画小应用
-
闲言碎语不多讲,咱们说说css3的transform属性:先上效果:效果说明:当鼠标移到a标签的时候,从右上角滑出二维码。实现方法:HTML代码如下:需要说明的一点是,a链接的跳转需要用javasc...
- CSS基础知识(七)CSS背景
-
一、CSS背景属性1.背景颜色(background-color)属性值:transparent(透明的)或color(颜色)2.背景图片(background-image)属性值:none(没有)...
- CSS 水平居中方式二
-
<divid="parent"><!--定义子级元素--><divid="child">居中布局</div>...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- maven镜像 (69)
- undefined reference to (60)
- zip格式 (63)
- oracle over (62)
- date_format函数用法 (67)
- 在线代理服务器 (60)
- shell 字符串比较 (74)
- x509证书 (61)
- localhost (65)
- java.awt.headless (66)
- syn_sent (64)
- settings.xml (59)
- 弹出窗口 (56)
- applicationcontextaware (72)
- my.cnf (73)
- httpsession (62)
- pkcs7 (62)
- session cookie (63)
- java 生成uuid (58)
- could not initialize class (58)
- beanpropertyrowmapper (58)
- word空格下划线不显示 (73)
- jar文件 (60)
- jsp内置对象 (58)
- makefile编写规则 (58)